diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8df028c --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..97fb33f --- /dev/null +++ b/.eslintrc.js @@ -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 + } +}; diff --git a/.stylelintrc.js b/.stylelintrc.js new file mode 100644 index 0000000..783be94 --- /dev/null +++ b/.stylelintrc.js @@ -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 + } +}; \ No newline at end of file diff --git a/README.md b/README.md index 1e16141..133acca 100644 --- a/README.md +++ b/README.md @@ -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. @@ -7,8 +13,6 @@ Install dependencies and start webpack watcher. in another terminal, start a web server using python. - python2 -m 'SimpleHTTPServer' 1337 - # or - python3 -m 'http.server' 1337 + npm run serve -Visit site on http://localhost:1337 +Visit site on http://localhost:10667 diff --git a/assets/style.css b/assets/style.css index 0755835..7cbebd0 100644 --- a/assets/style.css +++ b/assets/style.css @@ -1,8 +1,9 @@ +/* === Base === */ body { background-color: #111; color: #aaa; 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; } @@ -67,29 +68,33 @@ footer { right: 0; } -article { +/* === /Base === */ +/* === Helpers === */ + +.blur { + filter: blur(25px); +} + +/* === /Helpers === */ +/* === Albums === */ + +.album { display: flex; align-items: center; } -figure { - margin: 1rem; -} - -article:hover, -article:focus { +.album:hover { background: #333; color: #fff; transform: scale(1.05, 1.05); transition: transform .4s linear; } -article:hover img { - transform: scale(1.5, 1.5) translateX(-1.5rem); - transition: transform .2s linear; +.album__cover { + margin: 1rem; } -figure img { +.album__cover__media { width: 10vw; height: 10vw; padding: 5px; @@ -98,7 +103,24 @@ figure img { 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; max-width: 80%; transform: translateY(-3em); @@ -106,27 +128,13 @@ figure img { grid-template-rows: 1fr 2fr; } -#selected-album figure { +.selected-album__cover { grid-column: 1; grid-row: 1 / 3; margin: 0; } -#selected-album span { - 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 { +.selected-album__cover__media { width: 100%; padding: 0; border: 0; @@ -134,15 +142,22 @@ figure img { object-fit: cover; } -@media (min-width: 600px) { - #albums { - display: grid; - grid-template-columns: auto auto auto; - grid-gap: 10px; - text-transform: uppercase; - } +.selected-album__summary { + align-self: end; + text-transform: uppercase; + padding: 0 1rem; +} - #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; top: 0; right: 0; @@ -155,6 +170,4 @@ figure img { } } -.blur { - filter: blur(25px); -} \ No newline at end of file +/* === /Selected album === */ \ No newline at end of file diff --git a/src/actions/index.js b/src/actions/index.js index efa37b2..a227aa0 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -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_OK = 'LOAD_ALBUMS_OK'; 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 setVisibilityFilter = filter => ({ diff --git a/src/components/album-list.jsx b/src/components/album-list.jsx index fea8a95..868ec1a 100644 --- a/src/components/album-list.jsx +++ b/src/components/album-list.jsx @@ -10,7 +10,7 @@ export default class AlbumList extends Component { } = this.props; const classNames = blurred ? 'blur' : '' return ( -
+
{albums.map(album => ( handleOnClick(album)}> -
- cover +
handleOnClick(album)}> +
+ cover
#{id+1}: {artist} - {song}, från "{title}" ({year})
✔️ {purchased_on}
- ) + ); } } diff --git a/src/components/modal.jsx b/src/components/modal.jsx index d364ad8..98afe92 100644 --- a/src/components/modal.jsx +++ b/src/components/modal.jsx @@ -18,15 +18,15 @@ export default class Modal extends Component { const imagePath = `assets/covers/${img}`; const song = songs.join(', '); return ( -
-
-
- cover +
+
+
+ cover
- + #{id+1}: {artist} - {song}, från "{title}" ({year})
-

+

{description.split('\n\n').map(text => ( {text} @@ -37,6 +37,6 @@ export default class Modal extends Component {

- ) + ); } } diff --git a/src/index.js b/src/index.js index 3eb9e15..e74936c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import "babel-polyfill"; +import 'babel-polyfill'; import React from 'react'; import { render } from 'react-dom'; import { createStore, applyMiddleware, compose } from 'redux'; @@ -29,4 +29,4 @@ render( document.getElementById('brutal') ); -store.dispatch({ type: 'LOAD_ALBUMS', payload: { source: '/json/albums.json' }}); +store.dispatch({ type: 'LOAD_ALBUMS', payload: { source: 'json/albums.json' }}); diff --git a/src/sagas/index.js b/src/sagas/index.js index 9efecb7..52af7ed 100644 --- a/src/sagas/index.js +++ b/src/sagas/index.js @@ -1,25 +1,25 @@ import { put, takeEvery, all, call } from 'redux-saga/effects'; import { LOAD_ALBUMS, albumsLoadedOk } from '../actions'; -export function* loadAlbumsAsync(action) { - 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); - } +function* watchLoadAlbumsAsync() { + yield takeEvery(LOAD_ALBUMS, loadAlbumsAsync); } -export function* watchLoadAlbumsAsync() { - yield takeEvery(LOAD_ALBUMS, loadAlbumsAsync); +function* loadAlbumsAsync(action) { + 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* () { - yield all([ - watchLoadAlbumsAsync(), - ]) + yield all([ + watchLoadAlbumsAsync(), + ]); };