Lint code and introduce BEM

This commit is contained in:
Anders Ytterström 2019-04-23 15:18:35 +02:00
parent d447143312
commit 055511e555
11 changed files with 361 additions and 78 deletions

15
.editorconfig Normal file
View file

@ -0,0 +1,15 @@
# Based on the React EditorConfig file. See:
# - http://editorconfig.org
# - https://github.com/facebook/react/blob/master/.editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.{html,js,jsx,json,scss,css,yml}]
indent_size = 4

216
.eslintrc.js Normal file
View file

@ -0,0 +1,216 @@
var OFF = 0, WARN = 1, ERROR = 2;
module.exports = exports = {
"env": {
"es6": true
},
"ecmaFeatures": {
// env=es6 doesn't include modules, which we are using
"modules": true
},
"extends": "eslint:recommended",
"rules": {
// Possible Errors (overrides from recommended set)
"no-extra-parens": ERROR,
"no-unexpected-multiline": ERROR,
// All JSDoc comments must be valid
"valid-jsdoc": [ ERROR, {
"requireReturn": false,
"requireReturnDescription": false,
"requireParamDescription": true,
"prefer": {
"return": "returns"
}
}],
// Best Practices
// Allowed a getter without setter, but all setters require getters
"accessor-pairs": [ ERROR, {
"getWithoutSet": false,
"setWithoutGet": true
}],
"block-scoped-var": WARN,
"consistent-return": ERROR,
"curly": ERROR,
"default-case": WARN,
// the dot goes with the property when doing multiline
"dot-location": [ WARN, "property" ],
"dot-notation": WARN,
"eqeqeq": [ ERROR, "smart" ],
"guard-for-in": WARN,
"no-alert": ERROR,
"no-caller": ERROR,
"no-case-declarations": WARN,
"no-div-regex": WARN,
"no-else-return": WARN,
"no-empty-label": WARN,
"no-empty-pattern": WARN,
"no-eq-null": WARN,
"no-eval": ERROR,
"no-extend-native": ERROR,
"no-extra-bind": WARN,
"no-floating-decimal": WARN,
"no-implicit-coercion": [ WARN, {
"boolean": true,
"number": true,
"string": true
}],
"no-implied-eval": ERROR,
"no-invalid-this": ERROR,
"no-iterator": ERROR,
"no-labels": WARN,
"no-lone-blocks": WARN,
"no-loop-func": ERROR,
"no-magic-numbers": WARN,
"no-multi-spaces": ERROR,
"no-multi-str": WARN,
"no-native-reassign": ERROR,
"no-new-func": ERROR,
"no-new-wrappers": ERROR,
"no-new": ERROR,
"no-octal-escape": ERROR,
"no-param-reassign": ERROR,
"no-process-env": WARN,
"no-proto": ERROR,
"no-redeclare": ERROR,
"no-return-assign": ERROR,
"no-script-url": ERROR,
"no-self-compare": ERROR,
"no-throw-literal": ERROR,
"no-unused-expressions": ERROR,
"no-useless-call": ERROR,
"no-useless-concat": ERROR,
"no-void": WARN,
// Produce warnings when something is commented as TODO or FIXME
"no-warning-comments": [ WARN, {
"terms": [ "TODO", "FIXME" ],
"location": "start"
}],
"no-with": WARN,
"radix": WARN,
"vars-on-top": ERROR,
// Enforces the style of wrapped functions
"wrap-iife": [ ERROR, "outside" ],
"yoda": ERROR,
// Strict Mode - for ES6, never use strict.
"strict": [ ERROR, "never" ],
// Variables
"init-declarations": [ ERROR, "always" ],
"no-catch-shadow": WARN,
"no-delete-var": ERROR,
"no-label-var": ERROR,
"no-shadow-restricted-names": ERROR,
"no-shadow": WARN,
// We require all vars to be initialized (see init-declarations)
// If we NEED a var to be initialized to undefined, it needs to be explicit
"no-undef-init": OFF,
"no-undef": ERROR,
"no-undefined": OFF,
"no-unused-vars": WARN,
// Disallow hoisting - let & const don't allow hoisting anyhow
"no-use-before-define": ERROR,
// Node.js and CommonJS
"callback-return": [ WARN, [ "callback", "next" ]],
"global-require": ERROR,
"handle-callback-err": WARN,
"no-mixed-requires": WARN,
"no-new-require": ERROR,
// Use path.concat instead
"no-path-concat": ERROR,
"no-process-exit": ERROR,
"no-restricted-modules": OFF,
"no-sync": WARN,
// ECMAScript 6 support
"arrow-body-style": [ ERROR, "always" ],
"arrow-parens": [ ERROR, "always" ],
"arrow-spacing": [ ERROR, { "before": true, "after": true }],
"constructor-super": ERROR,
"generator-star-spacing": [ ERROR, "before" ],
"no-arrow-condition": ERROR,
"no-class-assign": ERROR,
"no-const-assign": ERROR,
"no-dupe-class-members": ERROR,
"no-this-before-super": ERROR,
"no-var": WARN,
"object-shorthand": [ WARN, "never" ],
"prefer-arrow-callback": WARN,
"prefer-spread": WARN,
"prefer-template": WARN,
"require-yield": ERROR,
// Stylistic - everything here is a warning because of style.
"array-bracket-spacing": [ WARN, "always" ],
"block-spacing": [ WARN, "always" ],
"brace-style": [ WARN, "1tbs", { "allowSingleLine": false } ],
"camelcase": WARN,
"comma-spacing": [ WARN, { "before": false, "after": true } ],
"comma-style": [ WARN, "last" ],
"computed-property-spacing": [ WARN, "never" ],
"consistent-this": [ WARN, "self" ],
"eol-last": WARN,
"func-names": WARN,
"func-style": [ WARN, "declaration" ],
"id-length": [ WARN, { "min": 2, "max": 32 } ],
"indent": [ WARN, 4 ],
"jsx-quotes": [ WARN, "prefer-double" ],
"linebreak-style": [ WARN, "unix" ],
"lines-around-comment": [ WARN, { "beforeBlockComment": true } ],
"max-depth": [ WARN, 8 ],
"max-len": [ WARN, 132 ],
"max-nested-callbacks": [ WARN, 8 ],
"max-params": [ WARN, 8 ],
"new-cap": WARN,
"new-parens": WARN,
"no-array-constructor": WARN,
"no-bitwise": OFF,
"no-continue": OFF,
"no-inline-comments": OFF,
"no-lonely-if": WARN,
"no-mixed-spaces-and-tabs": WARN,
"no-multiple-empty-lines": WARN,
"no-negated-condition": OFF,
"no-nested-ternary": WARN,
"no-new-object": WARN,
"no-plusplus": OFF,
"no-spaced-func": WARN,
"no-ternary": OFF,
"no-trailing-spaces": WARN,
"no-underscore-dangle": WARN,
"no-unneeded-ternary": WARN,
"object-curly-spacing": [ WARN, "always" ],
"one-var": OFF,
"operator-assignment": [ WARN, "never" ],
"operator-linebreak": [ WARN, "after" ],
"padded-blocks": [ WARN, "never" ],
"quote-props": [ WARN, "consistent-as-needed" ],
"quotes": [ WARN, "single" ],
"require-jsdoc": [ WARN, {
"require": {
"FunctionDeclaration": true,
"MethodDefinition": true,
"ClassDeclaration": false
}
}],
"semi-spacing": [ WARN, { "before": false, "after": true }],
"semi": [ ERROR, "always" ],
"sort-vars": OFF,
"space-after-keywords": [ WARN, "always" ],
"space-before-blocks": [ WARN, "always" ],
"space-before-function-paren": [ WARN, "never" ],
"space-before-keywords": [ WARN, "always" ],
"space-in-parens": [ WARN, "never" ],
"space-infix-ops": [ WARN, { "int32Hint": true } ],
"space-return-throw-case": ERROR,
"space-unary-ops": ERROR,
"spaced-comment": [ WARN, "always" ],
"wrap-regex": WARN
}
};

35
.stylelintrc.js Normal file
View file

@ -0,0 +1,35 @@
"use strict";
module.exports = {
rules: {
"at-rule-no-unknown": true,
"block-no-empty": true,
"color-no-invalid-hex": true,
"comment-no-empty": true,
"declaration-block-no-duplicate-properties": [
true,
{
ignore: ["consecutive-duplicates-with-different-values"]
}
],
"declaration-block-no-shorthand-property-overrides": true,
"font-family-no-duplicate-names": true,
"font-family-no-missing-generic-family-keyword": true,
"function-calc-no-unspaced-operator": true,
"function-linear-gradient-no-nonstandard-direction": true,
"keyframe-declaration-no-important": true,
"media-feature-name-no-unknown": true,
"no-descending-specificity": true,
"no-duplicate-at-import-rules": true,
"no-duplicate-selectors": true,
"no-empty-source": true,
"no-extra-semicolons": true,
"no-invalid-double-slash-comments": true,
"property-no-unknown": true,
"selector-pseudo-class-no-unknown": true,
"selector-pseudo-element-no-unknown": true,
"selector-type-no-unknown": true,
"string-no-newline": true,
"unit-no-unknown": true
}
};

View file

@ -1,4 +1,10 @@
# Getting started # BRÜTAL LEGEND
Progress visualisation of the quest to own a vinyl copy of all songs older than 1990 used in [Brütal Legend](https://en.wikipedia.org/wiki/Br%C3%BCtal_Legend), where possible.
Also a project for learning CSS Grid layout, React, Redux and Redux-sagas.
## Getting started
Install dependencies and start webpack watcher. Install dependencies and start webpack watcher.
@ -7,8 +13,6 @@ Install dependencies and start webpack watcher.
in another terminal, start a web server using python. in another terminal, start a web server using python.
python2 -m 'SimpleHTTPServer' 1337 npm run serve
# or
python3 -m 'http.server' 1337
Visit site on http://localhost:1337 Visit site on http://localhost:10667

View file

@ -1,8 +1,9 @@
/* === Base === */
body { body {
background-color: #111; background-color: #111;
color: #aaa; color: #aaa;
font-size: large; font-size: large;
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif;
padding: 5rem 10rem; padding: 5rem 10rem;
} }
@ -67,29 +68,33 @@ footer {
right: 0; right: 0;
} }
article { /* === /Base === */
/* === Helpers === */
.blur {
filter: blur(25px);
}
/* === /Helpers === */
/* === Albums === */
.album {
display: flex; display: flex;
align-items: center; align-items: center;
} }
figure { .album:hover {
margin: 1rem;
}
article:hover,
article:focus {
background: #333; background: #333;
color: #fff; color: #fff;
transform: scale(1.05, 1.05); transform: scale(1.05, 1.05);
transition: transform .4s linear; transition: transform .4s linear;
} }
article:hover img { .album__cover {
transform: scale(1.5, 1.5) translateX(-1.5rem); margin: 1rem;
transition: transform .2s linear;
} }
figure img { .album__cover__media {
width: 10vw; width: 10vw;
height: 10vw; height: 10vw;
padding: 5px; padding: 5px;
@ -98,7 +103,24 @@ figure img {
background-color: #000; background-color: #000;
} }
#selected-album > div { .album__cover__media:hover {
transform: scale(1.5, 1.5) translateX(-1.5rem);
transition: transform .2s linear;
}
@media (min-width: 600px) {
.albums {
display: grid;
grid-template-columns: auto auto auto;
grid-gap: 10px;
text-transform: uppercase;
}
}
/* === /Albums === */
/* === Selected album === */
.selected-album__inner {
display: grid; display: grid;
max-width: 80%; max-width: 80%;
transform: translateY(-3em); transform: translateY(-3em);
@ -106,27 +128,13 @@ figure img {
grid-template-rows: 1fr 2fr; grid-template-rows: 1fr 2fr;
} }
#selected-album figure { .selected-album__cover {
grid-column: 1; grid-column: 1;
grid-row: 1 / 3; grid-row: 1 / 3;
margin: 0; margin: 0;
} }
#selected-album span { .selected-album__cover__media {
align-self: end;
text-transform: uppercase;
padding: 0 1rem;
}
#selected-album p {
color: #fff;
margin: 0;
font-size: 1.5rem;
align-self: start;
padding: 0 1rem;
}
#selected-album img {
width: 100%; width: 100%;
padding: 0; padding: 0;
border: 0; border: 0;
@ -134,15 +142,22 @@ figure img {
object-fit: cover; object-fit: cover;
} }
@media (min-width: 600px) { .selected-album__summary {
#albums { align-self: end;
display: grid; text-transform: uppercase;
grid-template-columns: auto auto auto; padding: 0 1rem;
grid-gap: 10px; }
text-transform: uppercase;
}
#selected-album { .selected-album__description {
color: #fff;
margin: 0;
font-size: 1.5rem;
align-self: start;
padding: 0 1rem;
}
@media (min-width: 600px) {
.selected-album {
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
@ -155,6 +170,4 @@ figure img {
} }
} }
.blur { /* === /Selected album === */
filter: blur(25px);
}

View file

@ -1,8 +1,8 @@
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
export const SET_SORT_KEY = 'SET_SORT_KEY';
export const LOAD_ALBUMS = 'LOAD_ALBUMS'; export const LOAD_ALBUMS = 'LOAD_ALBUMS';
export const LOAD_ALBUMS_OK = 'LOAD_ALBUMS_OK'; export const LOAD_ALBUMS_OK = 'LOAD_ALBUMS_OK';
export const SELECT_ALBUM = 'SELECT_ALBUM'; export const SELECT_ALBUM = 'SELECT_ALBUM';
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
export const SET_SORT_KEY = 'SET_SORT_KEY';
export const UNSELECT_ALBUM = 'UNSELECT_ALBUM'; export const UNSELECT_ALBUM = 'UNSELECT_ALBUM';
export const setVisibilityFilter = filter => ({ export const setVisibilityFilter = filter => ({

View file

@ -10,7 +10,7 @@ export default class AlbumList extends Component {
} = this.props; } = this.props;
const classNames = blurred ? 'blur' : '' const classNames = blurred ? 'blur' : ''
return ( return (
<div id="albums" className={classNames}> <div className={"albums " + classNames}>
{albums.map(album => ( {albums.map(album => (
<Album <Album
key={album.id} key={album.id}

View file

@ -2,7 +2,7 @@ import React, { Component } from 'react';
export default class Album extends Component { export default class Album extends Component {
render() { render() {
const { album, handleOnClick } = this.props const { album, handleOnClick } = this.props;
const { const {
id, id,
artist, artist,
@ -15,15 +15,15 @@ export default class Album extends Component {
const imagePath = `assets/covers/${img}`; const imagePath = `assets/covers/${img}`;
const song = songs.join(', '); const song = songs.join(', ');
return ( return (
<article onClick={() => handleOnClick(album)}> <article className="album" onClick={() => handleOnClick(album)}>
<figure> <figure className="album__cover">
<img src={imagePath} alt="cover" /> <img src={imagePath} alt="cover" className="album__cover__media" />
</figure> </figure>
<span> <span>
#{id+1}: {artist} - {song}, från "{title}" ({year})<br /> #{id+1}: {artist} - {song}, från "{title}" ({year})<br />
<small> {purchased_on}</small> <small> {purchased_on}</small>
</span> </span>
</article> </article>
) );
} }
} }

View file

@ -18,15 +18,15 @@ export default class Modal extends Component {
const imagePath = `assets/covers/${img}`; const imagePath = `assets/covers/${img}`;
const song = songs.join(', '); const song = songs.join(', ');
return ( return (
<div id="selected-album" tabIndex="0" onClick={handleOnClick}> <div className="selected-album" tabIndex="0" onClick={handleOnClick}>
<div> <div className="selected-album__inner">
<figure> <figure className="selected-album__cover">
<img src={imagePath} alt="cover" /> <img src={imagePath} alt="cover" className="selected-album__cover__media" />
</figure> </figure>
<span> <span className="selected-album__summary">
#{id+1}: {artist} - {song}, från "{title}" ({year})<br /> #{id+1}: {artist} - {song}, från "{title}" ({year})<br />
</span> </span>
<p> <p className="selected-album__description">
{description.split('\n\n').map(text => ( {description.split('\n\n').map(text => (
<React.Fragment> <React.Fragment>
{text} {text}
@ -37,6 +37,6 @@ export default class Modal extends Component {
</p> </p>
</div> </div>
</div> </div>
) );
} }
} }

View file

@ -1,4 +1,4 @@
import "babel-polyfill"; import 'babel-polyfill';
import React from 'react'; import React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { createStore, applyMiddleware, compose } from 'redux'; import { createStore, applyMiddleware, compose } from 'redux';
@ -29,4 +29,4 @@ render(
document.getElementById('brutal') document.getElementById('brutal')
); );
store.dispatch({ type: 'LOAD_ALBUMS', payload: { source: '/json/albums.json' }}); store.dispatch({ type: 'LOAD_ALBUMS', payload: { source: 'json/albums.json' }});

View file

@ -1,25 +1,25 @@
import { put, takeEvery, all, call } from 'redux-saga/effects'; import { put, takeEvery, all, call } from 'redux-saga/effects';
import { LOAD_ALBUMS, albumsLoadedOk } from '../actions'; import { LOAD_ALBUMS, albumsLoadedOk } from '../actions';
export function* loadAlbumsAsync(action) { function* watchLoadAlbumsAsync() {
try { yield takeEvery(LOAD_ALBUMS, loadAlbumsAsync);
const { source } = action.payload;
const data = yield call(() => fetch(source)
.then(response => response.json())
.then(data => data), {}
);
yield put(albumsLoadedOk(data));
} catch (error) {
yield console.error(error);
}
} }
export function* watchLoadAlbumsAsync() { function* loadAlbumsAsync(action) {
yield takeEvery(LOAD_ALBUMS, loadAlbumsAsync); try {
const { source } = action.payload;
const data = yield call(() => fetch(source)
.then(response => response.json())
.then(data => data), {}
);
yield put(albumsLoadedOk(data));
} catch (error) {
yield console.error(error);
}
} }
export default function* () { export default function* () {
yield all([ yield all([
watchLoadAlbumsAsync(), watchLoadAlbumsAsync(),
]) ]);
}; };