Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
032f611b70 | ||
| 0f01801801 | |||
|
|
7029ac9330 | ||
|
|
5307cd3164 | ||
|
|
7677570ad2 | ||
|
|
79bbf9188c | ||
|
|
38f4244caf | ||
|
|
74732c1683 |
2
.babelrc
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"presets": ["@babel/preset-env", "@babel/react"]
|
"presets": ["react", "es2015", "stage-1"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
# 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
|
|
@ -1,216 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
9
.gitignore
vendored
|
|
@ -6,12 +6,3 @@ npm-debug.log
|
||||||
# IntelliJ
|
# IntelliJ
|
||||||
*.iml
|
*.iml
|
||||||
/.idea
|
/.idea
|
||||||
|
|
||||||
.cache
|
|
||||||
_build
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
docs
|
|
||||||
|
|
||||||
static/albums.json
|
|
||||||
static/covers
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
"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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
nodejs 12.10.0
|
|
||||||
8
LICENSE
|
|
@ -1,8 +0,0 @@
|
||||||
/*
|
|
||||||
* ----------------------------------------------------------------------------
|
|
||||||
* "THE BEER-WARE LICENSE" (Revision 42):
|
|
||||||
* <yttan@fastmail.se> wrote this code. As long as you retain this notice you
|
|
||||||
* can do whatever you want with this stuff. If we meet some day, and you think
|
|
||||||
* this stuff is worth it, you can buy me a beer in return. Anders Ytterström
|
|
||||||
* ----------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
38
README.md
|
|
@ -1,34 +1,14 @@
|
||||||
# BRÜTAL LEGEND
|
# Getting started
|
||||||
|
|
||||||
https://www.youtube.com/embed/VW88ofmfF0w
|
Install dependencies and start webpack watcher.
|
||||||
|
|
||||||
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.
|
npm install
|
||||||
|
npm run watch
|
||||||
|
|
||||||
Also a project for learning
|
in another terminal, start a web server using python.
|
||||||
|
|
||||||
- CSS Grid layout,
|
python2 -m 'SimpleHTTPServer' 1337
|
||||||
- React,
|
# or
|
||||||
- Redux,
|
python3 -m 'http.server' 1337
|
||||||
- Redux-sagas and
|
|
||||||
- TypeScript.
|
|
||||||
|
|
||||||
## Getting started
|
Visit site on http://localhost:1337
|
||||||
|
|
||||||
Install dependencies and start development server with live reload.
|
|
||||||
|
|
||||||
npm i
|
|
||||||
npm start
|
|
||||||
|
|
||||||
Visit site on http://localhost:1234
|
|
||||||
|
|
||||||
Use `npm run lint` to lint files.
|
|
||||||
|
|
||||||
## Build release
|
|
||||||
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
## Example data
|
|
||||||
|
|
||||||
The data should consist of a list of images (albums covers), and a JSON file. To have some example data to fiddle around with, a dummy data folder is available.
|
|
||||||
|
|
||||||
cp static/dummy-data/* static
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 271 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
BIN
assets/covers/06.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
assets/covers/07.jpg
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
assets/covers/08.jpg
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
assets/covers/09.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
assets/covers/10.jpg
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
assets/covers/11.jpg
Normal file
|
After Width: | Height: | Size: 443 KiB |
BIN
assets/covers/12.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
assets/covers/13.jpg
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
assets/covers/14.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
assets/covers/15.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
assets/covers/16.jpg
Normal file
|
After Width: | Height: | Size: 189 KiB |
106
assets/style.css
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
body {
|
||||||
|
background-color: #111;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: large;
|
||||||
|
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;
|
||||||
|
padding: 5rem 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link {
|
||||||
|
color: #a83;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: #a83;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover, a:focus {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:active {
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 3px solid #a83;
|
||||||
|
padding: 1.25rem 0;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
border-width: 0;
|
||||||
|
border-bottom: 3px solid #555;
|
||||||
|
font-size: large;
|
||||||
|
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
min-width: 17em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:hover {
|
||||||
|
background-color: #444;
|
||||||
|
border-color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #000;
|
||||||
|
border-color: #a83;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
color: #888;
|
||||||
|
font-size: small;
|
||||||
|
position: fixed;
|
||||||
|
transform: rotate(45deg) translate(2em, 3em);
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#albums {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto auto;
|
||||||
|
grid-gap: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
article {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article:hover,
|
||||||
|
article:focus {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure img {
|
||||||
|
width: 10vw;
|
||||||
|
height: 10vw;
|
||||||
|
padding: 5px;
|
||||||
|
display: block;
|
||||||
|
border: 1px solid #a83;
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
263
css/brutal.css
|
|
@ -1,263 +0,0 @@
|
||||||
/* === Base === */
|
|
||||||
body {
|
|
||||||
background-color: #111;
|
|
||||||
color: #aaa;
|
|
||||||
font-size: large;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
|
||||||
Helvetica Neue, Arial, sans-serif;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 80em;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:link {
|
|
||||||
color: #a83;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:visited {
|
|
||||||
color: #a83;
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover,
|
|
||||||
a:focus {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:active {
|
|
||||||
transform: translate(2px, 2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
border-bottom: 3px solid #a83;
|
|
||||||
padding: 0.5em 0.5em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 500px) {
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.5em;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
header {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === /Base === */
|
|
||||||
/* === Field === */
|
|
||||||
|
|
||||||
.field {
|
|
||||||
background-color: #333;
|
|
||||||
color: #fff;
|
|
||||||
border-width: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: large;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
|
||||||
Helvetica Neue, Arial, sans-serif;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field:hover {
|
|
||||||
background-color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field:focus {
|
|
||||||
background-color: #fff;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 500px) {
|
|
||||||
.field {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.field {
|
|
||||||
min-width: 17em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === /Field === */
|
|
||||||
/* === Helpers === */
|
|
||||||
|
|
||||||
.blur {
|
|
||||||
filter: blur(25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.visuallyhidden {
|
|
||||||
position: absolute;
|
|
||||||
left: -9999em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === /Helpers === */
|
|
||||||
/* === Albums === */
|
|
||||||
|
|
||||||
.album {
|
|
||||||
display: flex;
|
|
||||||
align-items: start;
|
|
||||||
padding: 0.5em;
|
|
||||||
margin: 0.8em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 500px) {
|
|
||||||
.album {
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
.album {
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.album:hover {
|
|
||||||
background: #333;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.album__cover {
|
|
||||||
margin: 0 0.5em 0 0;
|
|
||||||
padding-top: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 500px) {
|
|
||||||
.album__cover {
|
|
||||||
margin: 0 0 0.5em;
|
|
||||||
width: 100%;
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
.album__cover {
|
|
||||||
margin: 0 1em 0 0;
|
|
||||||
width: auto;
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.album__cover__media {
|
|
||||||
width: 25vw;
|
|
||||||
height: 25vw;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.album__cover__media:after {
|
|
||||||
position: absolute;
|
|
||||||
content: "";
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
background-color: #fff;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 500px) {
|
|
||||||
.album__cover__media {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
.album__cover__media {
|
|
||||||
width: 10vw;
|
|
||||||
height: 10vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 500px) {
|
|
||||||
.albums {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
grid-gap: 0.5em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
padding-bottom: 2em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === /Albums === */
|
|
||||||
/* === Selected album === */
|
|
||||||
|
|
||||||
.selected-album {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-album__inner {
|
|
||||||
overflow: auto;
|
|
||||||
border: 3px solid #a83;
|
|
||||||
background: black;
|
|
||||||
padding: 2em;
|
|
||||||
max-height: 80%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-album__summary {
|
|
||||||
text-transform: uppercase;
|
|
||||||
padding: 0.5em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-album__description {
|
|
||||||
color: #fff;
|
|
||||||
padding: 0 0.5rem 2em;
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 40em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-album__cover {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
.selected-album__cover {
|
|
||||||
display: block;
|
|
||||||
width: 75vh;
|
|
||||||
max-width: 900px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-album__media {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === /Selected album === */
|
|
||||||
|
|
@ -3,11 +3,12 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
<link rel="stylesheet" href="./css/brutal.css">
|
<link rel="stylesheet" href="assets/style.css">
|
||||||
<title>🤘 Brütal Legend 🤘</title>
|
<title>🤘 Brütal Legend 🤘</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="brutal"></div>
|
<div id="brutal"></div>
|
||||||
|
<footer>av <a href="https://madr.se" rel="author">madr</a> 2018</footer>
|
||||||
</body>
|
</body>
|
||||||
<script src="./src/index.tsx"></script>
|
<script src="bundle.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
26658
package-lock.json
generated
61
package.json
|
|
@ -1,38 +1,27 @@
|
||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env NODE_ENV=development parcel index.html --public-url / --out-dir _build",
|
"build": "webpack -p --define process.env.NODE_ENV='\"production\"' --progress --colors",
|
||||||
"build": "cross-env NODE_ENV=production parcel build index.html --public-url /bl/ --out-dir docs --no-source-maps --no-content-hash",
|
"watch": "webpack -p --define process.env.NODE_ENV='\"production\"' --watch --progress --colors",
|
||||||
"lint": "cross-env NODE_ENV=development prettier --check src/**/*"
|
"serve": "DEV_SERVER_PORT=10667 DEV_SERVER_ROOT=. dev-server"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"author": "",
|
||||||
"@babel/core": "^7.4.0",
|
"license": "ISC",
|
||||||
"@babel/plugin-transform-runtime": "^7.4.0",
|
"devDependencies": {
|
||||||
"@babel/preset-env": "^7.5.5",
|
"babel-core": "^6.2.1",
|
||||||
"@babel/preset-react": "^7.0.0",
|
"babel-loader": "^6.2.0",
|
||||||
"autoprefixer": "^9.5.0",
|
"babel-preset-es2015": "^6.1.18",
|
||||||
"cross-env": "^5.2.0",
|
"babel-preset-react": "^6.1.18",
|
||||||
"jest": "^24.9.0",
|
"redux-saga": "^1.0.2",
|
||||||
"parcel-bundler": "^1.12.3",
|
"webpack": "^1.12.9",
|
||||||
"parcel-plugin-static-files-copy": "^2.3.1",
|
"webpack-cli": "^3.2.3"
|
||||||
"prettier": "^1.18.2",
|
},
|
||||||
"typescript": "^3.6.3"
|
"dependencies": {
|
||||||
},
|
"babel-polyfill": "^6.26.0",
|
||||||
"dependencies": {
|
"babel-preset-stage-1": "^6.1.18",
|
||||||
"@babel/runtime-corejs2": "^7.4.2",
|
"dev-server": "^0.1.0",
|
||||||
"react": "^16.8.5",
|
"react": "16.3.2",
|
||||||
"react-dom": "^16.8.5",
|
"react-dom": ">=16.3.3",
|
||||||
"react-redux": "^6.0.1",
|
"react-redux": "5.0.7",
|
||||||
"redux": "^4.0.1",
|
"redux": "4.0.0"
|
||||||
"redux-saga": "^1.0.2",
|
}
|
||||||
"symbol-observable": "^4.0.0"
|
|
||||||
},
|
|
||||||
"postcss": {
|
|
||||||
"modules": false,
|
|
||||||
"plugins": {
|
|
||||||
"autoprefixer": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"browserslist": [
|
|
||||||
"defaults"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
src/actions/index.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
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 setVisibilityFilter = filter => ({
|
||||||
|
type: SET_VISIBILITY_FILTER,
|
||||||
|
payload: {
|
||||||
|
filter
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setSortKey = key => ({
|
||||||
|
type: SET_SORT_KEY,
|
||||||
|
payload: {
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const albumsLoadedOk = albums => ({
|
||||||
|
type: LOAD_ALBUMS_OK,
|
||||||
|
payload: {
|
||||||
|
albums,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import { Album } from "../interfaces";
|
|
||||||
|
|
||||||
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: string) => ({
|
|
||||||
type: SET_VISIBILITY_FILTER,
|
|
||||||
payload: {
|
|
||||||
filter
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setSortKey = (key: string) => ({
|
|
||||||
type: SET_SORT_KEY,
|
|
||||||
payload: {
|
|
||||||
key
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const albumsLoadedOk = (albums: Array<Album>) => ({
|
|
||||||
type: LOAD_ALBUMS_OK,
|
|
||||||
payload: {
|
|
||||||
albums
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const selectAlbum = (album: Album) => ({
|
|
||||||
type: SELECT_ALBUM,
|
|
||||||
payload: {
|
|
||||||
album
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const unselectAlbum = () => ({
|
|
||||||
type: UNSELECT_ALBUM
|
|
||||||
});
|
|
||||||
17
src/components/album-list.jsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Album from './album';
|
||||||
|
|
||||||
|
export default class AlbumList extends Component {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
albums,
|
||||||
|
} = this.props;
|
||||||
|
return (
|
||||||
|
<div id="albums">
|
||||||
|
{albums.map(album => (
|
||||||
|
<Album key={album.id} {...album} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import Album from "./album";
|
|
||||||
import * as interfaces from "../interfaces";
|
|
||||||
|
|
||||||
export default (props: Props) => {
|
|
||||||
const { albums, handleOnClick, blurred } = props;
|
|
||||||
const classNames = blurred ? "blur" : "";
|
|
||||||
return (
|
|
||||||
<div className={"albums " + classNames}>
|
|
||||||
{albums.map((album: interfaces.Album) => (
|
|
||||||
<Album
|
|
||||||
key={album.id}
|
|
||||||
album={album}
|
|
||||||
handleOnClick={handleOnClick}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
albums: Array<interfaces.Album>;
|
|
||||||
handleOnClick(album: interfaces.Album): void;
|
|
||||||
blurred: boolean;
|
|
||||||
};
|
|
||||||
28
src/components/album.jsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
export default class Album extends Component {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
artist,
|
||||||
|
title,
|
||||||
|
songs,
|
||||||
|
year,
|
||||||
|
img,
|
||||||
|
purchased_on,
|
||||||
|
} = this.props;
|
||||||
|
const imagePath = `assets/covers/${img}`;
|
||||||
|
const song = songs.join(', ');
|
||||||
|
return (
|
||||||
|
<article>
|
||||||
|
<figure>
|
||||||
|
<img src={imagePath} alt="cover" />
|
||||||
|
</figure>
|
||||||
|
<span>
|
||||||
|
#{id+1}: {artist} - {song}, från "{title}" ({year})<br />
|
||||||
|
<small>✔️ {purchased_on}</small>
|
||||||
|
</span>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Album } from "../interfaces";
|
|
||||||
|
|
||||||
export default (props: Props) => {
|
|
||||||
const handleKeyPress = (e: KeyboardEvent, callback: Function) => {
|
|
||||||
const SPACE_KEY = 32;
|
|
||||||
const ENTER_KEY = 13;
|
|
||||||
if (e.charCode === SPACE_KEY || e.charCode === ENTER_KEY) {
|
|
||||||
e.preventDefault();
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { album, handleOnClick } = props;
|
|
||||||
const { id, artist, title, songs, year, img, purchased_on } = album;
|
|
||||||
const imagePath = `./covers/${img}`;
|
|
||||||
const song = songs.join(", ");
|
|
||||||
return (
|
|
||||||
<article
|
|
||||||
className="album"
|
|
||||||
tabIndex={0}
|
|
||||||
role="button"
|
|
||||||
onClick={() => handleOnClick(album)}
|
|
||||||
>
|
|
||||||
<figure className="album__cover">
|
|
||||||
<img
|
|
||||||
src={imagePath}
|
|
||||||
alt="cover"
|
|
||||||
className="album__cover__media"
|
|
||||||
/>
|
|
||||||
</figure>
|
|
||||||
<span>
|
|
||||||
#{("00" + id).substr(-2, 2)}: {artist} - {song}, från "{title}" ({year})<br />
|
|
||||||
<small>✔️ {purchased_on}</small>
|
|
||||||
</span>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
key: number;
|
|
||||||
album: Album;
|
|
||||||
handleOnClick(album: Album): void;
|
|
||||||
};
|
|
||||||
18
src/components/app.jsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import AlbumListContainer from '../containers/album-list';
|
||||||
|
import FilterInputContainer from '../containers/filter-input';
|
||||||
|
//import SortSelectContainer from '../containers/sort-select';
|
||||||
|
|
||||||
|
export default class App extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<header>
|
||||||
|
<h1>Brütal Legend</h1>
|
||||||
|
<FilterInputContainer />
|
||||||
|
</header>
|
||||||
|
<AlbumListContainer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import AlbumList from "../containers/album-list";
|
|
||||||
import FilterInput from "../containers/filter-input";
|
|
||||||
import SortSelect from "../containers/sort-select";
|
|
||||||
import Modal from "../containers/modal";
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<React.Fragment>
|
|
||||||
<header>
|
|
||||||
<h1>Brütal Legend</h1>
|
|
||||||
<SortSelect />
|
|
||||||
<FilterInput />
|
|
||||||
</header>
|
|
||||||
<AlbumList />
|
|
||||||
<Modal />
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
20
src/components/filter-input.jsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
export default class FilterInput extends Component {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
handleOnChange,
|
||||||
|
} = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={value}
|
||||||
|
onChange={evt => handleOnChange(evt.target.value)}
|
||||||
|
placeholder='Filtrera på år, artist, låt, skivtitel ...'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export default (props: Props) => {
|
|
||||||
const { value, handleOnChange } = props;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={value}
|
|
||||||
className="field"
|
|
||||||
onChange={(evt: { target: HTMLInputElement }) =>
|
|
||||||
handleOnChange(evt.target.value)
|
|
||||||
}
|
|
||||||
placeholder="Filtrera på år, artist, låt, skivtitel ..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value: string;
|
|
||||||
handleOnChange(filterValue: string): void;
|
|
||||||
};
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Album } from "../interfaces";
|
|
||||||
|
|
||||||
export default (props: Props) => {
|
|
||||||
const handleKeyPress = (
|
|
||||||
keyPressed: string,
|
|
||||||
albumId: number,
|
|
||||||
close: Function
|
|
||||||
//goto: Function
|
|
||||||
) => {
|
|
||||||
if (keyPressed === "Escape") {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
// } else if (keyPressed === "ArrowRight") {
|
|
||||||
// goto(albumId, 1);
|
|
||||||
// } else if (keyPressed === "ArrowLeft") {
|
|
||||||
// goto(albumId, -1);
|
|
||||||
// }
|
|
||||||
};
|
|
||||||
|
|
||||||
const { album, close /*goto*/ } = props;
|
|
||||||
const { id, artist, title, songs, year, img, description } = album;
|
|
||||||
|
|
||||||
if (id === undefined) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const imagePath = `./covers/${img}`;
|
|
||||||
const song = songs.join(", ");
|
|
||||||
|
|
||||||
document.onkeyup = (e: KeyboardEvent) =>
|
|
||||||
handleKeyPress(e.key, album.id, close /*goto*/);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="selected-album blurred"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => close()}
|
|
||||||
>
|
|
||||||
<div className="selected-album__inner">
|
|
||||||
<figure className="selected-album__cover">
|
|
||||||
<img
|
|
||||||
src={imagePath}
|
|
||||||
alt="cover"
|
|
||||||
className="selected-album__media"
|
|
||||||
/>
|
|
||||||
</figure>
|
|
||||||
<span className="selected-album__summary">
|
|
||||||
#{("00" + id).substr(-2, 2)}: {artist} - {song}, från "{title}" ({year})
|
|
||||||
<br />
|
|
||||||
</span>
|
|
||||||
<div className="selected-album__description">
|
|
||||||
{description.split("\n\n").map(text => (
|
|
||||||
<p key={text}>{text}</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
album: Album;
|
|
||||||
close(): void;
|
|
||||||
// goto(albumId: number, direction: number): void;
|
|
||||||
};
|
|
||||||
20
src/components/sort-select.jsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
export default class SortSelect extends Component {
|
||||||
|
render() {
|
||||||
|
const { value, handleOnChange } = this.props;
|
||||||
|
return (
|
||||||
|
<div hidden>
|
||||||
|
<label htmlFor="sortBy">Sortera efter</label>
|
||||||
|
<select
|
||||||
|
id="sortBy"
|
||||||
|
value={value}
|
||||||
|
onChange={evt => handleOnChange(evt.target.value)}>
|
||||||
|
<option value="id">Inköpsdatum</option>
|
||||||
|
<option value="artist">Artist</option>
|
||||||
|
<option value="year">År</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export default (props: Props) => {
|
|
||||||
const { value, handleOnChange } = props;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<label htmlFor="sortBy" className="visuallyhidden">
|
|
||||||
Sortera efter
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="sortBy"
|
|
||||||
value={value}
|
|
||||||
className="field"
|
|
||||||
onChange={(evt: { target: HTMLSelectElement }) =>
|
|
||||||
handleOnChange(evt.target.value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="id">Inköpsdatum</option>
|
|
||||||
<option value="artist">Artist</option>
|
|
||||||
<option value="year">År</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value: string;
|
|
||||||
handleOnChange(sortKey: string): void;
|
|
||||||
};
|
|
||||||
18
src/containers/album-list.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import AlbumList from '../components/album-list';
|
||||||
|
|
||||||
|
const getAlbums = (albums, filter) => {
|
||||||
|
const atos = o => [o.artist, o.title, o.songs.join(' '), o.year].join(' ').toLowerCase();
|
||||||
|
if (filter) {
|
||||||
|
const term = filter.toLowerCase();
|
||||||
|
return albums.filter(album => atos(album).match(term));
|
||||||
|
}
|
||||||
|
return albums;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
albums: getAlbums(state.albums, state.visibilityFilter),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(AlbumList);
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { connect } from "react-redux";
|
|
||||||
import AlbumList from "../components/album-list";
|
|
||||||
import { selectAlbum } from "../actions";
|
|
||||||
import { Album, State } from "../interfaces";
|
|
||||||
|
|
||||||
const atos = (o: Album) =>
|
|
||||||
[o.artist, o.title, o.songs.join(" "), o.year].join(" ").toLowerCase();
|
|
||||||
|
|
||||||
const getAlbums = (albums: Array<Object>, filter: string, sortKey: string) => {
|
|
||||||
if (filter) {
|
|
||||||
const term = filter.toLowerCase();
|
|
||||||
albums = albums.filter((album: Album) => atos(album).match(term));
|
|
||||||
}
|
|
||||||
return [...albums].sort((a: Album, b: Album) =>
|
|
||||||
a[sortKey] > b[sortKey] ? 1 : -1
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = (state: State) => ({
|
|
||||||
albums: getAlbums(state.albums, state.visibilityFilter, state.sortKey),
|
|
||||||
blurred: "id" in state.selectedAlbum,
|
|
||||||
sortKey: state.sortKey
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: Function) => ({
|
|
||||||
handleOnClick: (album: Album) => dispatch(selectAlbum(album))
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(AlbumList);
|
|
||||||
14
src/containers/filter-input.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import FilterInput from '../components/filter-input';
|
||||||
|
import { setVisibilityFilter } from '../actions';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
value: state.visibilityFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
handleOnChange: filter => dispatch(setVisibilityFilter(filter)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(FilterInput);
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { connect } from "react-redux";
|
|
||||||
import FilterInput from "../components/filter-input";
|
|
||||||
import { setVisibilityFilter } from "../actions";
|
|
||||||
import { State } from "../interfaces";
|
|
||||||
|
|
||||||
const mapStateToProps = (state: State) => ({
|
|
||||||
value: state.visibilityFilter
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: Function) => ({
|
|
||||||
handleOnChange: (filter: string) => dispatch(setVisibilityFilter(filter))
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(FilterInput);
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { connect } from "react-redux";
|
|
||||||
import Modal from "../components/modal";
|
|
||||||
import { unselectAlbum } from "../actions";
|
|
||||||
import { State } from "../interfaces";
|
|
||||||
|
|
||||||
const mapStateToProps = (state: State) => ({
|
|
||||||
album: state.selectedAlbum
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: Function) => ({
|
|
||||||
close: () => dispatch(unselectAlbum())
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Modal);
|
|
||||||
14
src/containers/sort-select.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import SortSelect from '../components/sort-select';
|
||||||
|
import {setSortKey} from "../actions";
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
value: state.sortKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
handleOnChange: (key) => dispatch(setSortKey(key))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(SortSelect);
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { connect } from "react-redux";
|
|
||||||
import SortSelect from "../components/sort-select";
|
|
||||||
import { setSortKey } from "../actions";
|
|
||||||
import { State } from "../interfaces";
|
|
||||||
|
|
||||||
const mapStateToProps = (state: State) => ({
|
|
||||||
value: state.sortKey
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: Function) => ({
|
|
||||||
handleOnChange: (key: string) => dispatch(setSortKey(key))
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(SortSelect);
|
|
||||||
32
src/index.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import "babel-polyfill";
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import { createStore, applyMiddleware } from 'redux';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import createSagaMiddleware from 'redux-saga';
|
||||||
|
import rootReducer from './reducers';
|
||||||
|
import rootSagas from './sagas';
|
||||||
|
import App from './components/app';
|
||||||
|
|
||||||
|
const sagaMiddleware = createSagaMiddleware()
|
||||||
|
|
||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
const composeEnhancers =
|
||||||
|
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
const store = createStore(
|
||||||
|
rootReducer,
|
||||||
|
composeEnhancers(applyMiddleware(sagaMiddleware)),
|
||||||
|
)
|
||||||
|
|
||||||
|
sagaMiddleware.run(rootSagas)
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById('brutal')
|
||||||
|
);
|
||||||
|
|
||||||
|
store.dispatch({ type: 'LOAD_ALBUMS', payload: { source: '/json/albums.json' }});
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { render } from "react-dom";
|
|
||||||
import { createStore, applyMiddleware, compose } from "redux";
|
|
||||||
import { Provider } from "react-redux";
|
|
||||||
import createSagaMiddleware from "redux-saga";
|
|
||||||
import rootReducer from "./reducers";
|
|
||||||
import rootSagas from "./sagas";
|
|
||||||
import App from "./components/app";
|
|
||||||
import { LOAD_ALBUMS } from "./actions";
|
|
||||||
|
|
||||||
const sagaMiddleware = createSagaMiddleware();
|
|
||||||
|
|
||||||
/* eslint-disable no-underscore-dangle */
|
|
||||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
|
||||||
/* eslint-enable */
|
|
||||||
|
|
||||||
const store = createStore(
|
|
||||||
rootReducer,
|
|
||||||
composeEnhancers(applyMiddleware(sagaMiddleware))
|
|
||||||
);
|
|
||||||
|
|
||||||
sagaMiddleware.run(rootSagas);
|
|
||||||
|
|
||||||
render(
|
|
||||||
<Provider store={store}>
|
|
||||||
<App />
|
|
||||||
</Provider>,
|
|
||||||
document.getElementById("brutal")
|
|
||||||
);
|
|
||||||
|
|
||||||
store.dispatch({ type: LOAD_ALBUMS, payload: { source: "./albums.json" } });
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
export interface Album {
|
|
||||||
id: number;
|
|
||||||
artist: string;
|
|
||||||
title: string;
|
|
||||||
songs: Array<string>;
|
|
||||||
year: number;
|
|
||||||
img: string;
|
|
||||||
purchased_on: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface State {
|
|
||||||
albums: Array<Album>;
|
|
||||||
selectedAlbum: Album;
|
|
||||||
visibilityFilter: string;
|
|
||||||
sortKey: string;
|
|
||||||
}
|
|
||||||
11
src/reducers/albums.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { LOAD_ALBUMS_OK } from '../actions';
|
||||||
|
|
||||||
|
export default (state = [], action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case LOAD_ALBUMS_OK:
|
||||||
|
const { albums } = action.payload;
|
||||||
|
return albums;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { LOAD_ALBUMS_OK } from "../actions";
|
|
||||||
import { Album } from "../interfaces";
|
|
||||||
|
|
||||||
type Action = {
|
|
||||||
type: string;
|
|
||||||
payload: {
|
|
||||||
albums: Array<Album>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default (state: Array<Album> = [], action: Action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case LOAD_ALBUMS_OK:
|
|
||||||
const { albums } = action.payload;
|
|
||||||
return albums;
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
10
src/reducers/index.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { combineReducers } from 'redux';
|
||||||
|
import albums from './albums';
|
||||||
|
import visibilityFilter from './visibility-filter';
|
||||||
|
import sortKey from "./sort-key";
|
||||||
|
|
||||||
|
export default combineReducers({
|
||||||
|
albums,
|
||||||
|
visibilityFilter,
|
||||||
|
sortKey,
|
||||||
|
});
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { combineReducers } from "redux";
|
|
||||||
import albums from "./albums";
|
|
||||||
import visibilityFilter from "./visibility-filter";
|
|
||||||
import sortKey from "./sort-key";
|
|
||||||
import selectedAlbum from "./selected-album";
|
|
||||||
|
|
||||||
export default combineReducers({
|
|
||||||
albums,
|
|
||||||
visibilityFilter,
|
|
||||||
sortKey,
|
|
||||||
selectedAlbum
|
|
||||||
});
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { SELECT_ALBUM, UNSELECT_ALBUM } from "../actions";
|
|
||||||
import { Album } from "../interfaces";
|
|
||||||
|
|
||||||
type Action = {
|
|
||||||
type: string;
|
|
||||||
payload: {
|
|
||||||
album: Album;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default (state: Object = {}, action: Action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case SELECT_ALBUM:
|
|
||||||
return action.payload.album;
|
|
||||||
case UNSELECT_ALBUM:
|
|
||||||
return {};
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
10
src/reducers/sort-key.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { SET_SORT_KEY } from '../actions';
|
||||||
|
|
||||||
|
export default (state = 'id', action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SET_SORT_KEY:
|
||||||
|
return action.payload.key;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { SET_SORT_KEY } from "../actions";
|
|
||||||
|
|
||||||
type Action = {
|
|
||||||
type: string;
|
|
||||||
payload: {
|
|
||||||
key: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default (state: string = "id", action: Action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case SET_SORT_KEY:
|
|
||||||
return action.payload.key;
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
10
src/reducers/visibility-filter.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { SET_VISIBILITY_FILTER } from '../actions';
|
||||||
|
|
||||||
|
export default (state = '', action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SET_VISIBILITY_FILTER:
|
||||||
|
return action.payload.filter;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { SET_VISIBILITY_FILTER } from "../actions";
|
|
||||||
|
|
||||||
type Action = {
|
|
||||||
type: string;
|
|
||||||
payload: {
|
|
||||||
filter: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default (state: string = "", action: Action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case SET_VISIBILITY_FILTER:
|
|
||||||
return action.payload.filter;
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { put, takeEvery, all, call } from "redux-saga/effects";
|
|
||||||
import { LOAD_ALBUMS, albumsLoadedOk } from "../actions";
|
|
||||||
|
|
||||||
function* watchLoadAlbumsAsync() {
|
|
||||||
yield takeEvery(LOAD_ALBUMS, loadAlbumsAsync);
|
|
||||||
}
|
|
||||||
|
|
||||||
function* loadAlbumsAsync(action: LoadAlbumsAction) {
|
|
||||||
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()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoadAlbumsAction {
|
|
||||||
payload: {
|
|
||||||
source: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 6,
|
|
||||||
"img": "05.jpg",
|
|
||||||
"title": "Death or Glory",
|
|
||||||
"artist": "Running Wild",
|
|
||||||
"album": "Death or Glory",
|
|
||||||
"year": 1989,
|
|
||||||
"purchased_on": "2016-04-09",
|
|
||||||
"description": "",
|
|
||||||
"songs": ["Riding the Storm"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"img": "04.jpg",
|
|
||||||
"title": "Blizzard of Ozz",
|
|
||||||
"artist": "Ozzy Osbourne",
|
|
||||||
"album": "Blizzard of Ozz",
|
|
||||||
"year": 1980,
|
|
||||||
"purchased_on": "2016-04-05",
|
|
||||||
"description": "",
|
|
||||||
"songs": ["Mr Crowley"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"img": "03.jpg",
|
|
||||||
"title": "Destroyer",
|
|
||||||
"artist": "KISS",
|
|
||||||
"album": "Destroyer",
|
|
||||||
"year": 1976,
|
|
||||||
"purchased_on": "2016-03-19",
|
|
||||||
"description": "",
|
|
||||||
"songs": ["God of Thunder"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"img": "02.jpg",
|
|
||||||
"title": "Battle Cry",
|
|
||||||
"artist": "Omen",
|
|
||||||
"album": "Battle Cry",
|
|
||||||
"year": 1984,
|
|
||||||
"purchased_on": "2016-03-12",
|
|
||||||
"description": "",
|
|
||||||
"songs": ["The Axeman"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"img": "01.jpg",
|
|
||||||
"title": "O.F.R.",
|
|
||||||
"artist": "Nitro",
|
|
||||||
"album": "O.F.R.",
|
|
||||||
"year": 1989,
|
|
||||||
"purchased_on": "2016-01-31",
|
|
||||||
"description": "",
|
|
||||||
"songs": ["Machine Gunn Eddie"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"img": "00.jpg",
|
|
||||||
"title": "Refuge Denied",
|
|
||||||
"artist": "Sanctuary",
|
|
||||||
"album": "Refuge Denied",
|
|
||||||
"year": 1987,
|
|
||||||
"purchased_on": "2016-01-24",
|
|
||||||
"description": "",
|
|
||||||
"songs": ["Battle Angels"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "react",
|
|
||||||
"baseUrl": "./src",
|
|
||||||
"paths": {
|
|
||||||
"~*": ["./*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"]
|
|
||||||
}
|
|
||||||
30
webpack.config.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
module.exports = {
|
||||||
|
entry: ['./src/index.js'],
|
||||||
|
output: {
|
||||||
|
path: __dirname,
|
||||||
|
publicPath: '/',
|
||||||
|
filename: 'bundle.js'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
exclude: /node_modules/,
|
||||||
|
loader: 'babel',
|
||||||
|
query: {
|
||||||
|
presets: ['react', 'es2015', 'stage-1']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['', '.js', '.jsx']
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
historyApiFallback: true,
|
||||||
|
contentBase: './',
|
||||||
|
watchOptions: {
|
||||||
|
aggregateTimeout: 300,
|
||||||
|
poll: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||