├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── gulp ├── helpers │ └── jimp │ │ └── image-to-buffer.js ├── paths.js ├── plugins-options.js └── tasks │ ├── build.js │ ├── default.js │ ├── format-styles.js │ ├── images.js │ ├── scripts.js │ ├── styles.js │ ├── templates.js │ └── watch.js ├── gulpfile.babel.js ├── package-lock.json ├── package.json ├── plop-templates └── block │ ├── block.css │ └── block.pug ├── plopfile.babel.js ├── source ├── blocks │ ├── book │ │ ├── book.css │ │ └── book.pug │ ├── checkbox │ │ ├── checkbox.css │ │ └── checkbox.pug │ ├── container │ │ └── container.css │ ├── filters │ │ ├── filters.css │ │ └── filters.pug │ ├── footer │ │ ├── footer.css │ │ └── footer.pug │ ├── grid │ │ ├── grid.css │ │ └── grid.pug │ ├── head │ │ └── head.pug │ ├── header │ │ ├── header.css │ │ └── header.pug │ ├── link │ │ ├── link.css │ │ └── link.pug │ ├── logo │ │ ├── logo.css │ │ └── logo.pug │ ├── menu │ │ ├── menu.css │ │ └── menu.pug │ ├── nobr │ │ └── nobr.css │ ├── page │ │ └── page.css │ └── tag │ │ ├── tag.css │ │ └── tag.pug ├── data │ ├── dev.json │ └── site.json ├── images │ └── logo.png ├── layouts │ └── main.pug ├── pages │ └── index.pug ├── scripts │ ├── book.js │ ├── filters.js │ ├── helpers │ │ ├── curry.js │ │ ├── hide-node.js │ │ ├── hide-node.spec.js │ │ ├── is-in-collection.js │ │ ├── is-in-collection.spec.js │ │ ├── not-strict-equals.js │ │ ├── not-strict-equals.spec.js │ │ ├── show-node.js │ │ ├── show-node.spec.js │ │ ├── strict-equals.js │ │ └── strict-equals.spec.js │ └── index.js └── styles │ ├── base.css │ ├── config.css │ ├── index.css │ └── reset.css └── stylelint.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["syntax-object-rest-spread", "transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | charset = utf-8 9 | indent_style = tab 10 | indent_size = 4 11 | 12 | [*.{json,stylelintrc,eslintrc,babelrc}] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "globals": { 4 | "window": true, 5 | "document": true 6 | }, 7 | "rules": { 8 | "indent": ["error", "tab", { 9 | "SwitchCase": 1 10 | }], 11 | "no-tabs": "off", 12 | "no-multiple-empty-lines": ["error", { 13 | "max": 1 14 | }], 15 | "newline-after-var": ["error", "always"], 16 | "arrow-body-style": ["warn", "as-needed"], 17 | "arrow-parens": ["error", "always"], 18 | "valid-jsdoc": ["error", { 19 | "requireParamDescription": false, 20 | "requireReturnDescription": false 21 | }], 22 | "no-param-reassign": ["warn"], 23 | "import/no-named-as-default": ["warn"], 24 | "import/no-extraneous-dependencies": ["error", { 25 | "devDependencies": ["**/gulp/**/*.js"] 26 | }] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General files 2 | *~ 3 | .DS_Store 4 | 5 | # Build 6 | build/ 7 | 8 | # Development stuff 9 | node_modules/ 10 | *.sublime-workspace 11 | *.pyc 12 | .vagrant 13 | .idea 14 | .vscode 15 | npm-debug.* 16 | 17 | # Secure stuff 18 | ftp-config.json 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | script: 5 | - npm run qa 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Andrey Romanov 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 | [![Build Status](https://travis-ci.org/andrew--r/frontendbookshelf.svg?branch=master)](https://travis-ci.org/andrew--r/frontendbookshelf) 2 | 3 | # Книжная полка фронтендера 4 | 5 | ```bash 6 | $ yarn # install dependencies 7 | 8 | $ yarn build # alias for build:development 9 | $ yarn build:development 10 | 11 | $ yarn build:production 12 | 13 | $ yarn generate block # scaffold new block 14 | ``` 15 | -------------------------------------------------------------------------------- /gulp/helpers/jimp/image-to-buffer.js: -------------------------------------------------------------------------------- 1 | import jimp from 'jimp'; 2 | 3 | /** 4 | * Converts jimp image to buffer asynchronously 5 | * 6 | * @param {JIMPImage} image 7 | * @param {Object} options 8 | * @param {JIMPMimeType} options.mimeType 9 | * @return {Promise} resolve -> Buffer, reject -> error 10 | */ 11 | export default function jimpImageToBuffer(image, options = {}) { 12 | const { mimeType = jimp.AUTO } = options; 13 | 14 | return new Promise((resolve, reject) => { 15 | image.getBuffer(mimeType, (error, buffer) => { 16 | if (error) { 17 | reject(error); 18 | } else { 19 | resolve(buffer); 20 | } 21 | }); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /gulp/paths.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | source: { 5 | base: path.resolve(__dirname, '../source'), 6 | templates: { 7 | blocks: path.resolve(__dirname, '../source/blocks'), 8 | layouts: path.resolve(__dirname, '../source/layouts'), 9 | pages: path.resolve(__dirname, '../source/pages'), 10 | }, 11 | scripts: path.resolve(__dirname, '../source/scripts'), 12 | styles: { 13 | allStylesGlob: `${path.resolve(__dirname, '../source')}/{styles,blocks}`, 14 | common: path.resolve(__dirname, '../source/styles'), 15 | blocks: path.resolve(__dirname, '../source/blocks'), 16 | }, 17 | images: { 18 | site: path.resolve(__dirname, '../source/images'), 19 | booksCovers: path.resolve(__dirname, '../node_modules/frontendbookshelf-data/data/covers'), 20 | }, 21 | data: path.resolve(__dirname, '../source/data'), 22 | }, 23 | build: { 24 | base: path.resolve(__dirname, '../build'), 25 | templates: path.resolve(__dirname, '../build'), 26 | scripts: path.resolve(__dirname, '../build'), 27 | styles: path.resolve(__dirname, '../build'), 28 | images: path.resolve(__dirname, '../build/images'), 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /gulp/plugins-options.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import gutil from 'gulp-util'; 3 | import PATHS from './paths'; 4 | import stylelintConfig from '../stylelint.config'; 5 | 6 | const PLUGINS_OPTIONS = { 7 | plumber: { 8 | base: { 9 | errorHandler: gutil.log, 10 | }, 11 | }, 12 | pug: { 13 | development: { 14 | pretty: '\t', 15 | }, 16 | }, 17 | postcssEasyImport: { 18 | base: { 19 | glob: true, 20 | }, 21 | }, 22 | postcssSorting: { 23 | base: { 24 | 'rule-nested-empty-line-before': [true, { except: ['first-nested'] }], 25 | 'at-rule-nested-empty-line-before': [true, { except: ['first-nested'] }], 26 | 'declaration-empty-line-before': false, 27 | 'properties-order': stylelintConfig.rules['declaration-block-properties-order'], 28 | }, 29 | }, 30 | webpackStream: { 31 | base: { 32 | entry: path.resolve(process.cwd(), `${PATHS.source.scripts}/index.js`), 33 | output: { 34 | filename: 'bundle.js', 35 | }, 36 | resolve: { 37 | extensions: ['', '.js'], 38 | }, 39 | module: { 40 | loaders: [ 41 | { 42 | test: /\.jsx?$/, 43 | exclude: /(node_modules)/, 44 | loader: 'babel', 45 | }, 46 | ], 47 | }, 48 | }, 49 | development: { 50 | devtool: 'source-map', 51 | }, 52 | }, 53 | uglify: { 54 | base: { 55 | compress: { 56 | warnings: false, 57 | screw_ie8: true, 58 | }, 59 | }, 60 | }, 61 | browserSync: { 62 | base: { 63 | port: process.env.PORT || 3000, 64 | server: './build', 65 | open: 'local', 66 | }, 67 | }, 68 | }; 69 | 70 | /** 71 | * Returns plugin options based on current NODE_ENV 72 | * 73 | * @param {String} pluginName 74 | * @return {Object} plugin options based on NODE_ENV 75 | */ 76 | export default function getPluginOptions(pluginName) { 77 | const pluginOptions = PLUGINS_OPTIONS[pluginName] || {}; 78 | 79 | return { ...(pluginOptions.base || {}), ...(pluginOptions[process.env.NODE_ENV] || {}) }; 80 | } 81 | -------------------------------------------------------------------------------- /gulp/tasks/build.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | 3 | gulp.task('build', [ 4 | 'templates', 5 | 'styles', 6 | 'scripts', 7 | 'images', 8 | ]); 9 | -------------------------------------------------------------------------------- /gulp/tasks/default.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | 3 | gulp.task('default', () => {}); 4 | -------------------------------------------------------------------------------- /gulp/tasks/format-styles.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import plumber from 'gulp-plumber'; 3 | import postcss from 'gulp-postcss'; 4 | import sorting from 'postcss-sorting'; 5 | import PATHS from '../paths'; 6 | import getPluginOptions from '../plugins-options'; 7 | 8 | gulp.task('format:styles', () => { 9 | return gulp 10 | .src(`${PATHS.source.styles.allStylesGlob}/**/*.css`) 11 | .pipe(plumber(getPluginOptions('plumber'))) 12 | .pipe(postcss([ 13 | sorting(getPluginOptions('postcssSorting')), 14 | ])) 15 | .pipe(gulp.dest(PATHS.source.base)); 16 | }); 17 | -------------------------------------------------------------------------------- /gulp/tasks/images.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import imagemin from 'gulp-imagemin'; 3 | import jimp from 'jimp'; 4 | import through from 'through2'; 5 | import PATHS from '../paths'; 6 | import jimpImageToBuffer from '../helpers/jimp/image-to-buffer'; 7 | 8 | gulp.task('images', ['images:site', 'images:books-covers']); 9 | 10 | gulp.task('images:site', () => { 11 | return gulp 12 | .src(`${PATHS.source.images.site}/**/*`) 13 | .pipe(imagemin()) 14 | .pipe(gulp.dest(PATHS.build.images)); 15 | }); 16 | 17 | gulp.task('images:books-covers', () => { 18 | return gulp 19 | .src(`${PATHS.source.images.booksCovers}/**/*`) 20 | .pipe(through.obj(function (file, encoding, done) { 21 | const processedFile = file.clone(); 22 | 23 | jimp.read(file.contents) 24 | .then((image) => image.resize(300, jimp.AUTO)) 25 | .then(jimpImageToBuffer) 26 | .then((imageBuffer) => { 27 | processedFile.contents = imageBuffer; 28 | this.push(processedFile); 29 | }) 30 | .then(done) 31 | .catch(done); 32 | })) 33 | .pipe(imagemin()) 34 | .pipe(gulp.dest(PATHS.build.images)); 35 | }); 36 | -------------------------------------------------------------------------------- /gulp/tasks/scripts.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import gutil from 'gulp-util'; 3 | import plumber from 'gulp-plumber'; 4 | import webpackStream from 'webpack-stream'; 5 | import uglify from 'gulp-uglify'; 6 | import PATHS from '../paths'; 7 | import getPluginOptions from '../plugins-options'; 8 | 9 | const isProduction = process.env.NODE_ENV === 'production'; 10 | 11 | gulp.task('scripts', () => { 12 | return gulp 13 | .src(`${PATHS.source.scripts}/index.js`) 14 | .pipe(plumber(getPluginOptions('plumber'))) 15 | .pipe(webpackStream(getPluginOptions('webpackStream'))) 16 | .pipe(isProduction ? uglify(getPluginOptions('uglify')) : gutil.noop()) 17 | .pipe(gulp.dest(PATHS.build.scripts)); 18 | }); 19 | -------------------------------------------------------------------------------- /gulp/tasks/styles.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import plumber from 'gulp-plumber'; 3 | import rename from 'gulp-rename'; 4 | import postcss from 'gulp-postcss'; 5 | import easyImport from 'postcss-easy-import'; 6 | import cssnext from 'postcss-cssnext'; 7 | import reporter from 'postcss-reporter'; 8 | import flexbugsFixes from 'postcss-flexbugs-fixes'; 9 | import csso from 'postcss-csso'; 10 | import PATHS from '../paths'; 11 | import getPluginOptions from '../plugins-options'; 12 | 13 | gulp.task('styles', () => { 14 | return gulp 15 | .src(`${PATHS.source.styles.common}/index.css`) 16 | .pipe(plumber(getPluginOptions('plumber'))) 17 | .pipe(postcss([ 18 | easyImport(getPluginOptions('postcssEasyImport')), 19 | cssnext(), 20 | csso(), 21 | flexbugsFixes(), 22 | reporter({ clearAllMessages: true }), 23 | ])) 24 | .pipe(rename('main.css')) 25 | .pipe(gulp.dest(PATHS.build.styles)); 26 | }); 27 | -------------------------------------------------------------------------------- /gulp/tasks/templates.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import glob from 'glob'; 3 | import gulp from 'gulp'; 4 | import plumber from 'gulp-plumber'; 5 | import getData from 'gulp-data'; 6 | import pug from 'gulp-pug'; 7 | import rename from 'gulp-rename'; 8 | 9 | import { books, tags } from 'frontendbookshelf-data'; 10 | 11 | import PATHS from '../paths'; 12 | import getPluginOptions from '../plugins-options'; 13 | 14 | const parseDataFile = (filePath) => JSON.parse(fs.readFileSync(filePath)); 15 | const mergeObjects = (target, source) => ({ ...target, ...source }); 16 | 17 | gulp.task('templates', () => { 18 | return gulp 19 | .src(`${PATHS.source.templates.pages}/**/*.pug`) 20 | .pipe(plumber(getPluginOptions('plumber'))) 21 | .pipe(getData(() => glob 22 | .sync(`${PATHS.source.data}/**/*.json`) 23 | .map(parseDataFile) 24 | .concat({ books, tags }) 25 | .reduce(mergeObjects, {}))) 26 | .pipe(pug(getPluginOptions('pug'))) 27 | .pipe(rename((path) => { 28 | if (path.basename !== 'index') { 29 | path.dirname = path.basename; // eslint-disable-line no-param-reassign 30 | path.basename = 'index'; // eslint-disable-line no-param-reassign 31 | } 32 | })) 33 | .pipe(gulp.dest(PATHS.build.templates)); 34 | }); 35 | -------------------------------------------------------------------------------- /gulp/tasks/watch.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import browserSync from 'browser-sync'; 3 | import PATHS from '../paths'; 4 | import getPluginOptions from '../plugins-options'; 5 | 6 | const server = browserSync.create(); 7 | const { reload } = server; 8 | 9 | gulp.task('watch', ['build'], () => { 10 | server.init(getPluginOptions('browserSync')); 11 | 12 | gulp.watch([ 13 | `${PATHS.source.data}/**/*.json`, 14 | `${PATHS.source.templates.blocks}/**/*.pug`, 15 | `${PATHS.source.templates.layouts}/**/*.pug`, 16 | `${PATHS.source.templates.pages}/**/*.pug`, 17 | ], ['templates', reload]); 18 | 19 | gulp.watch([ 20 | `${PATHS.source.styles.common}/**/*.css`, 21 | `${PATHS.source.styles.blocks}/**/*.css`, 22 | ], ['styles', reload]); 23 | 24 | gulp.watch(`${PATHS.source.scripts}/**/*.js`, ['scripts', reload]); 25 | 26 | gulp.watch(`${PATHS.source.images}/**/*`, ['images', reload]); 27 | }); 28 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | require('require-dir')('./gulp/tasks', { recurse: true }); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontendbookshelf", 3 | "version": "1.0.0", 4 | "main": "", 5 | "repository": "https://github.com/andrew--r/frontendbookshelf", 6 | "author": "Andrew Romanov ", 7 | "license": "MIT", 8 | "engines": { 9 | "node": "^8.9.4", 10 | "npm": "^5.8.0" 11 | }, 12 | "scripts": { 13 | "generate": "plop", 14 | "clear": "rm -rf ./build", 15 | "prestart": "npm run clear", 16 | "start": "gulp watch", 17 | "format:styles": "gulp format:styles", 18 | "lint": "npm run lint:styles && npm run lint:scripts", 19 | "prelint:styles": "npm run format:styles", 20 | "lint:styles": "stylelint source/**/*.css", 21 | "lint:scripts": "eslint {gulp,source}", 22 | "test": "jest", 23 | "prebuild": "npm run clear", 24 | "build": "npm run build:production", 25 | "prebuild:production": "npm run clear", 26 | "build:production": "cross-env NODE_ENV=production gulp build", 27 | "prebuild:development": "npm run clear", 28 | "build:development": "cross-env NODE_ENV=development gulp build", 29 | "qa": "npm run lint && npm run test", 30 | "precommit": "npm run qa" 31 | }, 32 | "dependencies": { 33 | "frontendbookshelf-data": "1.1.0" 34 | }, 35 | "devDependencies": { 36 | "babel-core": "^6.21.0", 37 | "babel-loader": "^6.2.10", 38 | "babel-plugin-syntax-object-rest-spread": "^6.13.0", 39 | "babel-plugin-transform-object-rest-spread": "^6.22.0", 40 | "babel-preset-es2015": "^6.18.0", 41 | "bemto.pug": "^2.1.0", 42 | "browser-sync": "^2.18.6", 43 | "cross-env": "5.1.4", 44 | "eslint": "4.19.1", 45 | "eslint-config-airbnb": "16.1.0", 46 | "eslint-config-airbnb-base": "12.1.0", 47 | "eslint-plugin-import": "^2.2.0", 48 | "eslint-plugin-jsx-a11y": "6.0.3", 49 | "eslint-plugin-react": "7.7.0", 50 | "glob": "^7.1.1", 51 | "gulp": "^3.9.1", 52 | "gulp-data": "^1.2.1", 53 | "gulp-imagemin": "^3.1.1", 54 | "gulp-plumber": "^1.1.0", 55 | "gulp-postcss": "^6.2.0", 56 | "gulp-pug": "^3.2.0", 57 | "gulp-rename": "^1.2.2", 58 | "gulp-uglify": "^2.0.0", 59 | "gulp-util": "^3.0.8", 60 | "husky": "0.14.3", 61 | "jest": "22.4.3", 62 | "jimp": "^0.2.27", 63 | "plop": "^1.7.3", 64 | "postcss-cssnext": "^2.9.0", 65 | "postcss-csso": "^2.0.0", 66 | "postcss-easy-import": "^1.0.1", 67 | "postcss-flexbugs-fixes": "^3.0.0", 68 | "postcss-reporter": "^3.0.0", 69 | "postcss-sorting": "^2.0.1", 70 | "require-dir": "^0.3.1", 71 | "stylelint": "^7.7.1", 72 | "through2": "^2.0.3", 73 | "webpack-stream": "^3.2.0" 74 | }, 75 | "browserslist": [ 76 | "last 2 versions" 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /plop-templates/block/block.css: -------------------------------------------------------------------------------- 1 | .{{lowerCase name}} 2 | { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /plop-templates/block/block.pug: -------------------------------------------------------------------------------- 1 | mixin {{lowerCase name}}() 2 | +b.{{lowerCase name}}&attributes(attributes) 3 | -------------------------------------------------------------------------------- /plopfile.babel.js: -------------------------------------------------------------------------------- 1 | function isNotEmpty(name) { 2 | return (value) => { 3 | if (!value || value.trim() === '') return `${name} is required`; 4 | return true; 5 | }; 6 | } 7 | 8 | const { assign } = Object; 9 | const BLOCKS = './source/blocks'; 10 | const addStyle = { 11 | type: 'add', 12 | templateFile: 'plop-templates/block/block.css', 13 | }; 14 | const addMarkup = { 15 | type: 'add', 16 | templateFile: 'plop-templates/block/block.pug', 17 | }; 18 | 19 | function generateResourcePath(resourceType) { 20 | const resourcesNamesByType = { 21 | template: '{{lowerCase name}}.pug', 22 | style: '{{lowerCase name}}.css', 23 | }; 24 | 25 | return `${BLOCKS}/{{lowerCase name}}/${resourcesNamesByType[resourceType]}`; 26 | } 27 | 28 | module.exports = (plop) => { 29 | plop.setGenerator('block', { 30 | description: 'Create a new block', 31 | prompts: [{ 32 | type: 'input', 33 | name: 'name', 34 | message: 'What is your block name?', 35 | validate: isNotEmpty('name'), 36 | }], 37 | actions: [ 38 | assign({}, addStyle, { 39 | path: generateResourcePath('style'), 40 | }), 41 | assign({}, addMarkup, { 42 | path: generateResourcePath('template'), 43 | }), 44 | ], 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /source/blocks/book/book.css: -------------------------------------------------------------------------------- 1 | .book 2 | { 3 | position: relative; 4 | display: block; 5 | border: 0; 6 | } 7 | 8 | .book__thumb 9 | { 10 | position: relative; 11 | display: flex; 12 | flex-direction: column; 13 | align-items: flex-start; 14 | justify-content: flex-end; 15 | width: 100%; 16 | height: 360px; 17 | margin-bottom: 15px; 18 | } 19 | 20 | .book__thumb img 21 | { 22 | width: auto; 23 | max-height: 100%; 24 | border: 1px solid var(--border-color); 25 | } 26 | 27 | .book__title 28 | { 29 | display: block; 30 | font-size: 22px; 31 | font-weight: 700; 32 | border: 0; 33 | } 34 | 35 | .book__authors 36 | { 37 | display: block; 38 | padding-top: 5px; 39 | } 40 | 41 | .book__author 42 | { 43 | display: block; 44 | padding-bottom: 3px; 45 | font-size: 12px; 46 | letter-spacing: 1pt; 47 | text-transform: uppercase; 48 | color: rgba(0, 0, 0, .5); 49 | } 50 | -------------------------------------------------------------------------------- /source/blocks/book/book.pug: -------------------------------------------------------------------------------- 1 | mixin book(options) 2 | +b.book(data-tags=options.tags.join(','))&attributes(attributes) 3 | +e.A.thumb(href=options.url title=options.title) 4 | img(src=`images/${options.coverFilename}` alt=options.title) 5 | +e.SPAN.title=options.title 6 | +e.SPAN.authors 7 | each author in options.authors 8 | +e.SPAN.author=author 9 | -------------------------------------------------------------------------------- /source/blocks/checkbox/checkbox.css: -------------------------------------------------------------------------------- 1 | :root 2 | { 3 | --checkbox-font-size: 16px; 4 | --checkbox-size: .75em; 5 | --checkbox-border-radius: .1875em; 6 | --checkbox-box-shadow-size: .0625em; 7 | --checkbox-border-color: rgba(0, 0, 0, .2); 8 | 9 | --checkbox-outline-size: .1875em; 10 | --checkboux-outline-color: #91c6fd; 11 | 12 | --checkbox-background: #fff; 13 | --checkbox-background-active: rgba(0, 0, 0, .1); 14 | --checkbox-background-checked: #3b99fc; 15 | --checkbox-background-checked-active: #0a7ffb; 16 | 17 | --checkbox-check-width: .5em; 18 | --checkbox-check-height: .625em; 19 | --checkbox-check-top-offset: .125em; 20 | --checkbox-check-url: 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%228%22%20height%3D%2210%22%20viewBox%3D%223%203%208%2010%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20stroke%3D%22%23000%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20fill%3D%22none%22%20opacity%3D%22.2%22%20d%3D%22M3.9%208.91l2.102%202.384%204.23-6.534%22%2F%3E%3Cpath%20stroke%3D%22%23FFF%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20fill%3D%22none%22%20d%3D%22M3.9%207.91l2.102%202.384%204.23-6.534%22%2F%3E%3C%2Fsvg%3E'; 21 | } 22 | 23 | .checkbox 24 | { 25 | position: relative; 26 | font-size: var(--checkbox-font-size); 27 | } 28 | 29 | .checkbox__real 30 | { 31 | position: absolute; 32 | z-index: -1; 33 | opacity: 0; 34 | } 35 | 36 | .checkbox__fake 37 | { 38 | position: relative; 39 | display: inline-block; 40 | width: var(--checkbox-size); 41 | height: var(--checkbox-size); 42 | vertical-align: middle; 43 | border-radius: var(--checkbox-border-radius); 44 | background: var(--checkbox-background); 45 | box-shadow: 0 0 0 var(--checkbox-box-shadow-size) var(--checkbox-border-color), /* border */ 46 | inset 0 var(--checkbox-box-shadow-size) var(--checkbox-box-shadow-size) 0 var(--checkbox-border-color); /* top shadow */ 47 | } 48 | 49 | .checkbox__fake::before /* used for outline */ 50 | { 51 | content: ''; 52 | position: absolute; 53 | top: 0; 54 | right: 0; 55 | bottom: 0; 56 | left: 0; 57 | border-radius: var(--checkbox-border-radius); 58 | } 59 | 60 | .checkbox__real:focus + .checkbox__fake::before 61 | { 62 | box-shadow: 0 0 0 var(--checkbox-outline-size) var(--checkboux-outline-color); /* outline */ 63 | } 64 | 65 | .checkbox__real:checked + .checkbox__fake 66 | { 67 | background-color: var(--checkbox-background-checked); 68 | background-image: url(var(--checkbox-check-url)); 69 | background-repeat: no-repeat; 70 | background-position: center var(--checkbox-check-top-offset); 71 | background-size: var(--checkbox-check-width) var(--checkbox-check-height); 72 | box-shadow: 0 0 0 var(--checkbox-box-shadow-size) var(--checkbox-background-checked); /* border */ 73 | } 74 | 75 | .checkbox__real:not(:checked):active + .checkbox__fake 76 | { 77 | box-shadow: 0 0 0 var(--checkbox-outline-size) var(--checkboux-outline-color), /* outline */ 78 | 0 0 0 var(--checkbox-box-shadow-size) var(--checkbox-border-color), /* border */ 79 | inset 0 0 0 var(--checkbox-box-shadow-size) var(--checkbox-border-color), /* inner border */ 80 | inset 0 var(--checkbox-box-shadow-size) var(--checkbox-box-shadow-size) 0 var(--checkbox-border-color), /* top shadow */ 81 | inset 0 0 var(--checkbox-size) 0 var(--checkbox-background-active); /* background as box-shadow for lightness */ 82 | } 83 | 84 | .checkbox__real:checked:active + .checkbox__fake 85 | { 86 | background-color: var(--checkbox-background-checked-active); 87 | box-shadow: 0 0 0 var(--checkbox-outline-size) var(--checkboux-outline-color), /* outline */ 88 | 0 0 0 var(--checkbox-box-shadow-size) var(--checkbox-background-checked-active), /* border */ 89 | inset 0 0 0 var(--checkbox-box-shadow-size) color(var(--checkbox-background-checked-active) lightness(45%)); /* inner border */ 90 | } 91 | -------------------------------------------------------------------------------- /source/blocks/checkbox/checkbox.pug: -------------------------------------------------------------------------------- 1 | mixin checkbox(id) 2 | +b.SPAN.checkbox&attributes(attributes) 3 | +e.INPUT.real(type='checkbox' id=id) 4 | +e.SPAN.fake 5 | -------------------------------------------------------------------------------- /source/blocks/container/container.css: -------------------------------------------------------------------------------- 1 | .container 2 | { 3 | width: 100%; 4 | max-width: calc(var(--site-width) + var(--site-padding) * 2); 5 | margin-right: auto; 6 | margin-left: auto; 7 | padding-right: var(--site-padding); 8 | padding-left: var(--site-padding); 9 | } 10 | -------------------------------------------------------------------------------- /source/blocks/filters/filters.css: -------------------------------------------------------------------------------- 1 | .filters 2 | { 3 | padding-top: 15px; 4 | padding-bottom: 15px; 5 | line-height: 2; 6 | } 7 | 8 | .filters__item.tag 9 | { 10 | @media (--mobile) 11 | { 12 | display: block; 13 | margin-bottom: 10px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /source/blocks/filters/filters.pug: -------------------------------------------------------------------------------- 1 | include ../tag/tag 2 | 3 | mixin filters() 4 | +b.filters.container&attributes(attributes) 5 | each tagId in Object.keys(tags.dictionary) 6 | +tag(tagId, tags.dictionary[tagId]).filters__item 7 | -------------------------------------------------------------------------------- /source/blocks/footer/footer.css: -------------------------------------------------------------------------------- 1 | .footer 2 | { 3 | width: 100%; 4 | } 5 | 6 | .footer__credits 7 | { 8 | padding: 7px 0; 9 | font-size: 14px; 10 | } 11 | -------------------------------------------------------------------------------- /source/blocks/footer/footer.pug: -------------------------------------------------------------------------------- 1 | include ../link/link 2 | 3 | mixin footer() 4 | +b.footer.container&attributes(attributes) 5 | +e.P.credits Сделал в 2016–2017 #[+link()(href='http://andrew-r.ru') Андрей Романов] 6 | -------------------------------------------------------------------------------- /source/blocks/grid/grid.css: -------------------------------------------------------------------------------- 1 | :root 2 | { 3 | --grid-item-padding: 32px; 4 | --grid-item-width: 300px; 5 | } 6 | 7 | .grid 8 | { 9 | overflow: hidden; 10 | padding-top: 15px; 11 | } 12 | 13 | .grid__inner 14 | { 15 | display: flex; 16 | flex-wrap: wrap; 17 | align-items: flex-start; 18 | justify-content: space-around; 19 | margin-right: calc(var(--grid-item-padding) * -1); 20 | margin-left: calc(var(--grid-item-padding) * -1); 21 | } 22 | 23 | .grid__item 24 | { 25 | width: var(--grid-item-width); 26 | padding: var(--grid-item-padding); 27 | } 28 | 29 | .grid__item.js-last-visible 30 | { 31 | flex-basis: 100%; 32 | } 33 | 34 | .grid__item.js-hidden 35 | { 36 | display: none; 37 | } 38 | 39 | -------------------------------------------------------------------------------- /source/blocks/grid/grid.pug: -------------------------------------------------------------------------------- 1 | include ../book/book 2 | 3 | mixin grid() 4 | +b.grid.container&attributes(attributes) 5 | +e.inner 6 | each book in books.list 7 | +e.item 8 | +book(book) 9 | -------------------------------------------------------------------------------- /source/blocks/head/head.pug: -------------------------------------------------------------------------------- 1 | head 2 | meta(charset='utf-8') 3 | meta(name='viewport' content='width=device-width, initial-scale=1, minimal-ui') 4 | meta(http-equiv='X-UA-Compatible' content='IE=edge') 5 | 6 | meta(name='imagetoolbar' content='no') 7 | meta(name='msthemecompatible' content='no') 8 | meta(name='cleartype' content='on') 9 | meta(name='HandheldFriendly' content='True') 10 | 11 | // all kind of icons 12 | link(rel='apple-touch-icon', sizes='57x57', href='../apple-icon-57x57.png') 13 | link(rel='apple-touch-icon', sizes='60x60', href='../apple-icon-60x60.png') 14 | link(rel='apple-touch-icon', sizes='72x72', href='../apple-icon-72x72.png') 15 | link(rel='apple-touch-icon', sizes='76x76', href='../apple-icon-76x76.png') 16 | link(rel='apple-touch-icon', sizes='114x114', href='../apple-icon-114x114.png') 17 | link(rel='apple-touch-icon', sizes='120x120', href='../apple-icon-120x120.png') 18 | link(rel='apple-touch-icon', sizes='144x144', href='../apple-icon-144x144.png') 19 | link(rel='apple-touch-icon', sizes='152x152', href='../apple-icon-152x152.png') 20 | link(rel='apple-touch-icon', sizes='180x180', href='../apple-icon-180x180.png') 21 | link(rel='icon', type='image/png', sizes='192x192', href='../android-icon-192x192.png') 22 | link(rel='icon', type='image/png', sizes='32x32', href='../favicon-32x32.png') 23 | link(rel='icon', type='image/png', sizes='96x96', href='../favicon-96x96.png') 24 | link(rel='icon', type='image/png', sizes='16x16', href='../favicon-16x16.png') 25 | 26 | meta(name='msapplication-TileColor', content='#ffffff') 27 | meta(name='msapplication-TileImage', content='../ms-icon-144x144.png') 28 | meta(name='theme-color', content='#ffffff') 29 | 30 | // todo: add manifest 31 | // link(rel='manifest', href='manifest.json') 32 | 33 | meta(name='description' content=site.description) 34 | meta(name='keywords' content=site.keywords) 35 | 36 | block head 37 | title= pageTitle ? `${pageTitle} ${site.titleDelimiter} ${site.name}` : site.name 38 | 39 | link(href='https://fonts.googleapis.com/css?family=Cuprum' rel='stylesheet') 40 | link(href='main.css' rel='stylesheet') 41 | -------------------------------------------------------------------------------- /source/blocks/header/header.css: -------------------------------------------------------------------------------- 1 | .header 2 | { 3 | width: 100%; 4 | padding-top: 32px; 5 | padding-bottom: 20px; 6 | 7 | @media (--tablet) 8 | { 9 | padding-top: 26px; 10 | } 11 | 12 | @media (--mobile) 13 | { 14 | padding-top: 20px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /source/blocks/header/header.pug: -------------------------------------------------------------------------------- 1 | include ../logo/logo 2 | include ../menu/menu 3 | 4 | mixin header() 5 | +b.header.container 6 | +menu() 7 | +logo() 8 | -------------------------------------------------------------------------------- /source/blocks/link/link.css: -------------------------------------------------------------------------------- 1 | .link 2 | { 3 | text-decoration: none; 4 | color: var(--link-color); 5 | border-bottom: 1px solid color(var(--link-color) alpha(25%)); 6 | } 7 | 8 | .link:hover, 9 | .link.hover 10 | { 11 | color: var(--link-color-hover); 12 | border-bottom: 1px solid color(var(--link-color-hover) alpha(25%)); 13 | } 14 | -------------------------------------------------------------------------------- /source/blocks/link/link.pug: -------------------------------------------------------------------------------- 1 | mixin link() 2 | +b.A.link&attributes(attributes) 3 | block 4 | -------------------------------------------------------------------------------- /source/blocks/logo/logo.css: -------------------------------------------------------------------------------- 1 | .logo 2 | { 3 | font-family: Georgia, 'Times New Roman', Times, serif; 4 | font-size: 64px; 5 | line-height: 1; 6 | 7 | @media (--tablet) 8 | { 9 | font-size: 60px; 10 | } 11 | 12 | @media (--mobile) 13 | { 14 | font-size: 36px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /source/blocks/logo/logo.pug: -------------------------------------------------------------------------------- 1 | mixin logo() 2 | +b.H1.logo&attributes(attributes) Книжная полка фронтендера 3 | -------------------------------------------------------------------------------- /source/blocks/menu/menu.css: -------------------------------------------------------------------------------- 1 | .menu 2 | { 3 | padding-bottom: 16px; 4 | } 5 | 6 | .menu__delimiter 7 | { 8 | margin-right: 5px; 9 | margin-left: 5px; 10 | 11 | @media (--mobile) 12 | { 13 | display: none; 14 | } 15 | } 16 | 17 | .menu__item 18 | { 19 | @media (--mobile) 20 | { 21 | display: table; 22 | white-space: nowrap; 23 | } 24 | } 25 | 26 | .menu__item:not(:last-child) 27 | { 28 | @media (--mobile) 29 | { 30 | margin-bottom: 8px; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /source/blocks/menu/menu.pug: -------------------------------------------------------------------------------- 1 | mixin menu() 2 | +b.NAV.menu&attributes(attributes) 3 | +e.A.item.link(href='https://github.com/andrew--r/frontendbookshelf-data/issues/new') Добавить книгу ⇗ 4 | +e.SPAN.delimiter • 5 | +e.A.item.link(href='mailto:me@andrew-r.ru?subject=[frontendbookshelf] Сообщение о проблеме') Сообщить о проблеме ✉ 6 | -------------------------------------------------------------------------------- /source/blocks/nobr/nobr.css: -------------------------------------------------------------------------------- 1 | .nobr 2 | { 3 | white-space: nowrap; 4 | } 5 | -------------------------------------------------------------------------------- /source/blocks/page/page.css: -------------------------------------------------------------------------------- 1 | .page 2 | { 3 | display: flex; 4 | flex-direction: column; 5 | min-height: 100vh; 6 | } 7 | 8 | .page__header, 9 | .page__footer 10 | { 11 | flex-basis: auto; 12 | flex-grow: 0; 13 | flex-shrink: 0; 14 | } 15 | 16 | .page__content 17 | { 18 | flex-basis: auto; 19 | flex-grow: 1; 20 | flex-shrink: 0; 21 | } 22 | -------------------------------------------------------------------------------- /source/blocks/tag/tag.css: -------------------------------------------------------------------------------- 1 | :root 2 | { 3 | --tag-items-spacing: .25em; 4 | --tag-color: #4078c0; 5 | --tag-background-color: #e7f2ff; 6 | } 7 | 8 | .tag 9 | { 10 | display: inline-block; 11 | margin-right: .5em; 12 | padding: .25em var(--tag-items-spacing); 13 | line-height: 1; 14 | cursor: pointer; 15 | color: var(--tag-color); 16 | border: 1px solid var(--tag-background-color); 17 | border-radius: 3px; 18 | background-color: var(--tag-background-color); 19 | } 20 | 21 | .tag__checkbox 22 | { 23 | margin-right: var(--tag-items-spacing); 24 | margin-left: var(--tag-items-spacing); 25 | } 26 | 27 | .tag__content 28 | { 29 | padding-right: var(--tag-items-spacing); 30 | padding-left: var(--tag-items-spacing); 31 | } 32 | 33 | .tag__content:active 34 | { 35 | user-select: none; 36 | } 37 | -------------------------------------------------------------------------------- /source/blocks/tag/tag.pug: -------------------------------------------------------------------------------- 1 | include ../checkbox/checkbox 2 | 3 | mixin tag(id, name) 4 | +b.LABEL.tag(for=id)&attributes(attributes) 5 | +checkbox(id)(class='tag__checkbox') 6 | +e.SPAN.content=name 7 | -------------------------------------------------------------------------------- /source/data/dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "jv0": "javascript:void(0);" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /source/data/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "site": { 3 | "name": "Книжная полка фронтендера", 4 | "titleDelimiter": "|", 5 | "description": "«Книжная полка фронтендера» — это коллекция лучших книг для фронтенд-разработчиков. Книги можно фильтровать по тематике, языку и сложности, благодаря чему вы с лёгкостью найдёте то, что вам нужно.", 6 | "keywords": "вёрстка, фронтенд, веб-дизайн, веб-разработка, книги, учебная литература, учебники, html, css, javascript" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /source/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew--r/frontendbookshelf/633da8d4777cb4630474bec51c6cdc3454b1b6d2/source/images/logo.png -------------------------------------------------------------------------------- /source/layouts/main.pug: -------------------------------------------------------------------------------- 1 | include ../../node_modules/bemto.pug/bemto 2 | 3 | include ../blocks/header/header 4 | include ../blocks/footer/footer 5 | 6 | doctype html 7 | html(lang='ru') 8 | include ../blocks/head/head 9 | body 10 | +b.page 11 | +e.header 12 | +header() 13 | 14 | +e.content 15 | block pageContent 16 | 17 | +e.footer 18 | +footer() 19 | 20 | block scripts 21 | 22 | script(src='bundle.js') 23 | //- метрика 24 | script. 25 | (function (d, w, c) { (w[c] = w[c] || []).push(function() { try { w.yaCounter32702595 = new Ya.Metrika({ id:32702595, clickmap:true, trackLinks:true, accurateTrackBounce:true, webvisor:true }); } catch(e) { } }); var n = d.getElementsByTagName("script")[0], s = d.createElement("script"), f = function () { n.parentNode.insertBefore(s, n); }; s.type = "text/javascript"; s.async = true; s.src = "https://mc.yandex.ru/metrika/watch.js"; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "yandex_metrika_callbacks"); 26 | noscript 27 | div 28 | img(src='https://mc.yandex.ru/watch/32702595', style='position:absolute; left:-9999px;', alt='') 29 | -------------------------------------------------------------------------------- /source/pages/index.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/main 2 | include ../blocks/filters/filters 3 | include ../blocks/grid/grid 4 | 5 | block head 6 | - var pageTitle = 'Список книг'; 7 | 8 | block pageContent 9 | +filters() 10 | +grid() 11 | -------------------------------------------------------------------------------- /source/scripts/book.js: -------------------------------------------------------------------------------- 1 | import showNode from './helpers/show-node'; 2 | import hideNode from './helpers/hide-node'; 3 | 4 | export class Book { 5 | constructor(node) { 6 | this.node = node; 7 | this.tags = node.getAttribute('data-tags').split(','); 8 | } 9 | 10 | getNode() { 11 | return this.node; 12 | } 13 | 14 | getTags() { 15 | return this.tags; 16 | } 17 | 18 | show() { 19 | showNode(this.node.parentNode); 20 | } 21 | 22 | hide() { 23 | hideNode(this.node.parentNode); 24 | } 25 | } 26 | 27 | export function createBookFromNode(node) { 28 | return new Book(node); 29 | } 30 | -------------------------------------------------------------------------------- /source/scripts/filters.js: -------------------------------------------------------------------------------- 1 | import isInCollection from './helpers/is-in-collection'; 2 | import notStrictEquals from './helpers/not-strict-equals'; 3 | 4 | import { createBookFromNode } from './book'; 5 | 6 | export default class Filters { 7 | constructor(options) { 8 | this.options = options; 9 | this.booksList = Array 10 | .from(document.querySelectorAll(options.bookSelector)) 11 | .map(createBookFromNode); 12 | 13 | this.state = { 14 | currentTags: [], 15 | }; 16 | } 17 | 18 | init() { 19 | Array 20 | .from(document.querySelectorAll(this.options.tagCheckboxSelector)) 21 | .forEach((checkbox) => { 22 | checkbox.addEventListener('change', this.handleFilterToggle.bind(this)); 23 | }); 24 | } 25 | 26 | onUpdate() { 27 | const { currentTags } = this.state; 28 | 29 | if (currentTags.length) { 30 | this.booksList.forEach((book) => { 31 | const bookMatchesCurrentTags = currentTags.some(isInCollection(book.getTags())); 32 | 33 | if (bookMatchesCurrentTags) { 34 | book.show(); 35 | } else { 36 | book.hide(); 37 | } 38 | }); 39 | } else { 40 | this.booksList.forEach((book) => book.show()); 41 | } 42 | } 43 | 44 | handleFilterToggle(event) { 45 | const { target } = event; 46 | const { currentTags } = this.state; 47 | 48 | this.setState({ 49 | currentTags: target.checked ? 50 | currentTags.concat([target.id]) 51 | : 52 | currentTags.filter(notStrictEquals(target.id)), 53 | }); 54 | } 55 | 56 | /** 57 | * Sets new state 58 | * 59 | * @param {Object} newState 60 | * @return {undefined} 61 | */ 62 | setState(newState) { 63 | this.state = { 64 | ...this.state, 65 | ...newState, 66 | }; 67 | 68 | this.onUpdate(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /source/scripts/helpers/curry.js: -------------------------------------------------------------------------------- 1 | export default function curry(f) { 2 | return (...a) => (...b) => f(...a, ...b); 3 | } 4 | -------------------------------------------------------------------------------- /source/scripts/helpers/hide-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hides DOM Node 3 | * 4 | * @param {DOMNode} node 5 | * @return {undefined} 6 | */ 7 | export default function hideNode(node) { 8 | node.style.display = 'none'; 9 | } 10 | -------------------------------------------------------------------------------- /source/scripts/helpers/hide-node.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, test, expect */ 2 | 3 | import hideNode from './hide-node'; 4 | 5 | describe('hideNode()', () => { 6 | test('should set node style\'s display property to empty string', () => { 7 | const hiddenNode = { 8 | style: { display: '' }, 9 | }; 10 | 11 | hideNode(hiddenNode); 12 | expect(hiddenNode.style.display).toBe('none'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /source/scripts/helpers/is-in-collection.js: -------------------------------------------------------------------------------- 1 | import curry from './curry'; 2 | import strictEquals from './strict-equals'; 3 | 4 | /** 5 | * Checks if given item is presented in given collection 6 | * 7 | * @param {Array} collection 8 | * @param {*} item 9 | * @return {Boolean} 10 | */ 11 | export function isInCollection(collection, item) { 12 | return collection.some(strictEquals(item)); 13 | } 14 | 15 | export default curry(isInCollection); 16 | -------------------------------------------------------------------------------- /source/scripts/helpers/is-in-collection.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, test, expect */ 2 | 3 | import { isInCollection } from './is-in-collection'; 4 | 5 | describe('isInCollection()', () => { 6 | test('should check if item is presented in collection', () => { 7 | const person = { name: 'Andrew' }; 8 | const collection = [1, 'hello', person]; 9 | 10 | expect(isInCollection(collection, 1)).toBe(true); 11 | expect(isInCollection(collection, 'hello')).toBe(true); 12 | expect(isInCollection(collection, person)).toBe(true); 13 | expect(isInCollection(collection, 5)).toBe(false); 14 | expect(isInCollection(collection, null)).toBe(false); 15 | expect(isInCollection(collection, undefined)).toBe(false); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /source/scripts/helpers/not-strict-equals.js: -------------------------------------------------------------------------------- 1 | import curry from './curry'; 2 | 3 | /** 4 | * Checks if two values are not strictly equal 5 | * 6 | * @param {*} a 7 | * @param {*} b 8 | * @return {Boolean} 9 | */ 10 | export function notStrictEquals(a, b) { 11 | return a !== b; 12 | } 13 | 14 | export default curry(notStrictEquals); 15 | -------------------------------------------------------------------------------- /source/scripts/helpers/not-strict-equals.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, test, expect */ 2 | 3 | import { notStrictEquals } from './not-strict-equals'; 4 | 5 | describe('notStrictEquals()', () => { 6 | test('should check strict unequality', () => { 7 | expect(notStrictEquals(1, 1)).toBe(false); 8 | expect(notStrictEquals('1', '1')).toBe(false); 9 | expect(notStrictEquals(1, '1')).toBe(true); 10 | expect(notStrictEquals([], [])).toBe(true); 11 | expect(notStrictEquals({}, {})).toBe(true); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /source/scripts/helpers/show-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Shows DOM Node 3 | * 4 | * @param {DOMNode} node 5 | * @return {undefined} 6 | */ 7 | export default function showNode(node) { 8 | node.style.display = ''; 9 | } 10 | -------------------------------------------------------------------------------- /source/scripts/helpers/show-node.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, test, expect */ 2 | 3 | import showNode from './show-node'; 4 | 5 | describe('showNode()', () => { 6 | test('should set node style\'s display property to empty string', () => { 7 | const hiddenNode = { 8 | style: { display: 'none' }, 9 | }; 10 | 11 | showNode(hiddenNode); 12 | expect(hiddenNode.style.display).toBe(''); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /source/scripts/helpers/strict-equals.js: -------------------------------------------------------------------------------- 1 | import curry from './curry'; 2 | 3 | /** 4 | * Checks if two values are strictly equal 5 | * 6 | * @param {*} a 7 | * @param {*} b 8 | * @return {Boolean} 9 | */ 10 | export function strictEquals(a, b) { 11 | return a === b; 12 | } 13 | 14 | export default curry(strictEquals); 15 | -------------------------------------------------------------------------------- /source/scripts/helpers/strict-equals.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, test, expect */ 2 | 3 | import { strictEquals } from './strict-equals'; 4 | 5 | describe('strictEquals()', () => { 6 | test('should check strict equality', () => { 7 | expect(strictEquals(1, 1)).toBe(true); 8 | expect(strictEquals('1', '1')).toBe(true); 9 | expect(strictEquals(1, '1')).toBe(false); 10 | expect(strictEquals([], [])).toBe(false); 11 | expect(strictEquals({}, {})).toBe(false); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /source/scripts/index.js: -------------------------------------------------------------------------------- 1 | import Filters from './filters'; 2 | 3 | function onDOMContentLoaded() { 4 | const filters = new Filters({ 5 | bookSelector: '.book', 6 | tagCheckboxSelector: '.tag input[type="checkbox"]', 7 | }); 8 | 9 | filters.init(); 10 | } 11 | 12 | document.addEventListener('DOMContentLoaded', onDOMContentLoaded); 13 | -------------------------------------------------------------------------------- /source/styles/base.css: -------------------------------------------------------------------------------- 1 | html, 2 | body 3 | { 4 | height: 100%; 5 | font-family: var(--base-fonts-set); 6 | font-size: var(--base-font-size); 7 | line-height: var(--base-line-height); 8 | background-color: var(--site-background-color); 9 | } 10 | -------------------------------------------------------------------------------- /source/styles/config.css: -------------------------------------------------------------------------------- 1 | :root 2 | { 3 | --site-width: 1440px; 4 | --site-padding: 20px; 5 | --site-background-color: #fff; 6 | 7 | --base-fonts-set: Arial, Helvetica, sans-serif; 8 | --base-font-size: 16px; 9 | --base-line-height: 1.2; 10 | 11 | --link-color: #0072ba; 12 | --link-color-hover: #d04000; 13 | 14 | --border-color: rgba(0, 0, 0, .2); 15 | } 16 | 17 | @custom-media --mobile (width <= 480px); 18 | @custom-media --tablet (width < 1024px); 19 | @custom-media --desktop (width >= 1024px); 20 | -------------------------------------------------------------------------------- /source/styles/index.css: -------------------------------------------------------------------------------- 1 | @import './config.css'; 2 | @import './reset.css'; 3 | @import './base.css'; 4 | @import '../blocks/**/*.css'; 5 | -------------------------------------------------------------------------------- /source/styles/reset.css: -------------------------------------------------------------------------------- 1 | html 2 | { 3 | box-sizing: border-box; 4 | } 5 | 6 | *, 7 | *:before, 8 | *:after 9 | { 10 | box-sizing: inherit; 11 | } 12 | 13 | html, 14 | body, 15 | p, 16 | ol, 17 | ul, 18 | li, 19 | dl, 20 | dt, 21 | dd, 22 | blockquote, 23 | figure, 24 | fieldset, 25 | legend, 26 | textarea, 27 | pre, 28 | iframe, 29 | hr, 30 | h1, 31 | h2, 32 | h3, 33 | h4, 34 | h5, 35 | h6 36 | { 37 | margin: 0; 38 | padding: 0; 39 | } 40 | 41 | 42 | h1, 43 | h2, 44 | h3, 45 | h4, 46 | h5, 47 | h6 48 | { 49 | font-size: 100%; 50 | font-weight: 900; 51 | } 52 | 53 | h1 54 | { 55 | font-size: 42px; 56 | } 57 | 58 | h2 59 | { 60 | font-size: 30px; 61 | } 62 | 63 | h3 64 | { 65 | font-size: 22px; 66 | } 67 | 68 | ul 69 | { 70 | list-style: none; 71 | } 72 | 73 | button, 74 | input, 75 | select, 76 | textarea 77 | { 78 | margin: 0; 79 | } 80 | 81 | iframe 82 | { 83 | border: 0; 84 | } 85 | 86 | img 87 | { 88 | display: block; 89 | max-width: 100%; 90 | height: auto; 91 | } 92 | 93 | a 94 | { 95 | text-decoration: none; 96 | color: inherit; 97 | } 98 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | indentation: ['tab', { 4 | ignore: 'value', 5 | }], 6 | 'color-hex-case': 'lower', 7 | 'color-hex-length': 'short', 8 | 'at-rule-empty-line-before': ['always', { 9 | except: ['first-nested', 'blockless-after-same-name-blockless'], 10 | }], 11 | 'at-rule-semicolon-newline-after': 'always', 12 | 'block-closing-brace-newline-before': 'always', 13 | 'block-closing-brace-newline-after': 'always', 14 | 'block-opening-brace-newline-before': 'always', 15 | 'block-opening-brace-newline-after': 'always', 16 | 'declaration-colon-space-before': 'never', 17 | 'declaration-colon-space-after': 'always', 18 | 'length-zero-no-unit': true, 19 | 'number-leading-zero': 'never', 20 | 'number-no-trailing-zeros': true, 21 | 'selector-combinator-space-before': 'always', 22 | 'selector-combinator-space-after': 'always', 23 | 'selector-list-comma-space-before': 'never-single-line', 24 | 'selector-list-comma-space-after': 'never-single-line', 25 | 'selector-list-comma-newline-before': 'never-multi-line', 26 | 'selector-list-comma-newline-after': 'always', 27 | 'shorthand-property-no-redundant-values': true, 28 | 'string-quotes': 'single', 29 | 'declaration-block-properties-order': [ 30 | 'content', 31 | 'position', 32 | 'z-index', 33 | 'top', 34 | 'right', 35 | 'bottom', 36 | 'left', 37 | 'flex', 38 | 'flex-basis', 39 | 'flex-grow', 40 | 'flex-shrink', 41 | 'display', 42 | 'flex-wrap', 43 | 'flex-direction', 44 | 'flex-order', 45 | 'flex-pack', 46 | 'flex-align', 47 | 'align-items', 48 | 'justify-content', 49 | 'visibility', 50 | 'float', 51 | 'clear', 52 | 'overflow', 53 | 'overflow-x', 54 | 'overflow-y', 55 | 'overflow-scrolling', 56 | 'clip', 57 | 'zoom', 58 | 'box-sizing', 59 | 'width', 60 | 'min-width', 61 | 'max-width', 62 | 'height', 63 | 'min-height', 64 | 'max-height', 65 | 'margin', 66 | 'margin-top', 67 | 'margin-right', 68 | 'margin-bottom', 69 | 'margin-left', 70 | 'padding', 71 | 'padding-top', 72 | 'padding-right', 73 | 'padding-bottom', 74 | 'padding-left', 75 | 'table-layout', 76 | 'empty-cells', 77 | 'caption-side', 78 | 'border-spacing', 79 | 'border-collapse', 80 | 'list-style', 81 | 'list-style-position', 82 | 'list-style-type', 83 | 'list-style-image', 84 | 'quotes', 85 | 'counter-reset', 86 | 'counter-increment', 87 | 'font', 88 | 'font-family', 89 | 'font-size', 90 | 'font-weight', 91 | 'font-style', 92 | 'font-variant', 93 | 'font-size-adjust', 94 | 'font-stretch', 95 | 'font-effect', 96 | 'font-emphasize', 97 | 'font-emphasize-position', 98 | 'font-emphasize-style', 99 | 'font-smooth', 100 | 'line-height', 101 | 'text-align', 102 | 'text-align-last', 103 | 'vertical-align', 104 | 'white-space', 105 | 'text-decoration', 106 | 'text-emphasis', 107 | 'text-emphasis-color', 108 | 'text-emphasis-style', 109 | 'text-emphasis-position', 110 | 'text-indent', 111 | 'text-justify', 112 | 'text-transform', 113 | 'letter-spacing', 114 | 'word-spacing', 115 | 'writing-mode', 116 | 'text-outline', 117 | 'text-transform', 118 | 'text-wrap', 119 | 'text-overflow', 120 | 'text-overflow-ellipsis', 121 | 'text-overflow-mode', 122 | 'word-wrap', 123 | 'word-break', 124 | 'tab-size', 125 | 'hyphens', 126 | 'resize', 127 | 'cursor', 128 | 'user-select', 129 | 'nav-index', 130 | 'nav-up', 131 | 'nav-right', 132 | 'nav-down', 133 | 'nav-left', 134 | 'transition', 135 | 'transition-delay', 136 | 'transition-timing-function', 137 | 'transition-duration', 138 | 'transition-property', 139 | 'transform', 140 | 'transform-origin', 141 | 'animation', 142 | 'animation-name', 143 | 'animation-duration', 144 | 'animation-play-state', 145 | 'animation-timing-function', 146 | 'animation-delay', 147 | 'animation-iteration-count', 148 | 'animation-iteration-count', 149 | 'animation-direction', 150 | 'pointer-events', 151 | 'opacity', 152 | 'interpolation-mode', 153 | 'color', 154 | 'border', 155 | 'border-collapse', 156 | 'border-width', 157 | 'border-style', 158 | 'border-color', 159 | 'border-top', 160 | 'border-top-width', 161 | 'border-top-style', 162 | 'border-top-color', 163 | 'border-right', 164 | 'border-right-width', 165 | 'border-right-style', 166 | 'border-right-color', 167 | 'border-bottom', 168 | 'border-bottom-width', 169 | 'border-bottom-style', 170 | 'border-bottom-color', 171 | 'border-left', 172 | 'border-left-width', 173 | 'border-left-style', 174 | 'border-left-color', 175 | 'border-radius', 176 | 'border-top-left-radius', 177 | 'border-top-right-radius', 178 | 'border-bottom-right-radius', 179 | 'border-bottom-left-radius', 180 | 'border-image', 181 | 'border-image-source', 182 | 'border-image-slice', 183 | 'border-image-width', 184 | 'border-image-outset', 185 | 'border-image-repeat', 186 | 'outline', 187 | 'outline-width', 188 | 'outline-style', 189 | 'outline-color', 190 | 'outline-offset', 191 | 'background', 192 | 'background-color', 193 | 'background-image', 194 | 'background-repeat', 195 | 'background-attachment', 196 | 'background-position', 197 | 'background-position-x', 198 | 'background-position-y', 199 | 'background-clip', 200 | 'background-origin', 201 | 'background-size', 202 | 'box-decoration-break', 203 | 'box-shadow', 204 | 'text-shadow', 205 | ], 206 | }, 207 | }; 208 | --------------------------------------------------------------------------------