Compare commits
24 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45a6ea25c8 | |||
| da4f93a30b | |||
| bff9b2fdd7 | |||
|
|
ea6a48deef | ||
|
|
e0a39dd4d8 | ||
|
|
ecaa1271bf | ||
| 3839813a0a | |||
|
|
f5f5d61413 | ||
| 9bcf0ecc56 | |||
| 755cf14828 | |||
| 4e28567959 | |||
|
|
6ba958fafa | ||
|
|
cb9f3ec3d1 | ||
|
|
39f0542ef2 | ||
|
|
93358ac607 | ||
| e04e04bee9 | |||
|
|
055511e555 | ||
|
|
d447143312 | ||
|
|
1ceb248df9 | ||
|
|
60e70f4b34 | ||
|
|
c11b1d4653 | ||
|
|
3f0ac404f0 | ||
|
|
4266dbcf6b | ||
|
|
d722027781 |
2
.babelrc
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"presets": ["react", "es2015", "stage-1"]
|
||||
"presets": ["@babel/preset-env", "@babel/react"]
|
||||
}
|
||||
|
|
|
|||
15
.editorconfig
Normal 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
|
|
@ -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
|
||||
}
|
||||
};
|
||||
9
.gitignore
vendored
|
|
@ -6,3 +6,12 @@ npm-debug.log
|
|||
# IntelliJ
|
||||
*.iml
|
||||
/.idea
|
||||
|
||||
.cache
|
||||
_build
|
||||
.vscode
|
||||
|
||||
docs
|
||||
|
||||
static/albums.json
|
||||
static/covers
|
||||
35
.stylelintrc.js
Normal 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
|
||||
}
|
||||
};
|
||||
1
.tool-versions
Normal file
|
|
@ -0,0 +1 @@
|
|||
nodejs 12.10.0
|
||||
8
LICENSE
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* ----------------------------------------------------------------------------
|
||||
* "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
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
40
README.md
|
|
@ -1,14 +1,34 @@
|
|||
# Getting started
|
||||
# BRÜTAL LEGEND
|
||||
|
||||
Install dependencies and start webpack watcher.
|
||||
https://www.youtube.com/embed/VW88ofmfF0w
|
||||
|
||||
npm install
|
||||
npm run watch
|
||||
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.
|
||||
|
||||
in another terminal, start a web server using python.
|
||||
Also a project for learning
|
||||
|
||||
python2 -m 'SimpleHTTPServer' 1337
|
||||
# or
|
||||
python3 -m 'http.server' 1337
|
||||
|
||||
Visit site on http://localhost:1337
|
||||
- CSS Grid layout,
|
||||
- React,
|
||||
- Redux,
|
||||
- Redux-sagas and
|
||||
- TypeScript.
|
||||
|
||||
## Getting started
|
||||
|
||||
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: 128 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 443 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 189 KiB |
106
assets/style.css
|
|
@ -1,106 +0,0 @@
|
|||
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
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
/* === 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,12 +3,11 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
<link rel="stylesheet" href="./css/brutal.css">
|
||||
<title>🤘 Brütal Legend 🤘</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="brutal"></div>
|
||||
<footer>av <a href="https://madr.se" rel="author">madr</a> 2018</footer>
|
||||
</body>
|
||||
<script src="bundle.js"></script>
|
||||
<script src="./src/index.tsx"></script>
|
||||
</html>
|
||||
|
|
|
|||
26498
package-lock.json
generated
61
package.json
|
|
@ -1,27 +1,38 @@
|
|||
{
|
||||
"scripts": {
|
||||
"build": "webpack -p --define process.env.NODE_ENV='\"production\"' --progress --colors",
|
||||
"watch": "webpack -p --define process.env.NODE_ENV='\"production\"' --watch --progress --colors",
|
||||
"serve": "DEV_SERVER_PORT=10667 DEV_SERVER_ROOT=. dev-server"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.2.1",
|
||||
"babel-loader": "^6.2.0",
|
||||
"babel-preset-es2015": "^6.1.18",
|
||||
"babel-preset-react": "^6.1.18",
|
||||
"redux-saga": "^1.0.2",
|
||||
"webpack": "^1.12.9",
|
||||
"webpack-cli": "^3.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-preset-stage-1": "^6.1.18",
|
||||
"dev-server": "^0.1.0",
|
||||
"react": "16.3.2",
|
||||
"react-dom": ">=16.3.3",
|
||||
"react-redux": "5.0.7",
|
||||
"redux": "4.0.0"
|
||||
}
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development parcel index.html --public-url / --out-dir _build",
|
||||
"build": "cross-env NODE_ENV=production parcel build index.html --public-url /bl/ --out-dir docs --no-source-maps --no-content-hash",
|
||||
"lint": "cross-env NODE_ENV=development prettier --check src/**/*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.0",
|
||||
"@babel/plugin-transform-runtime": "^7.4.0",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"autoprefixer": "^9.5.0",
|
||||
"cross-env": "^5.2.0",
|
||||
"jest": "^24.9.0",
|
||||
"parcel-bundler": "^1.12.3",
|
||||
"parcel-plugin-static-files-copy": "^2.3.1",
|
||||
"prettier": "^1.18.2",
|
||||
"typescript": "^3.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime-corejs2": "^7.4.2",
|
||||
"react": "^16.8.5",
|
||||
"react-dom": "^16.8.5",
|
||||
"react-redux": "^6.0.1",
|
||||
"redux": "^4.0.1",
|
||||
"redux-saga": "^1.0.2",
|
||||
"symbol-observable": "^4.0.0"
|
||||
},
|
||||
"postcss": {
|
||||
"modules": false,
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
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,
|
||||
},
|
||||
})
|
||||
40
src/actions/index.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
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
|
||||
});
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
25
src/components/album-list.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
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;
|
||||
};
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
44
src/components/album.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
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;
|
||||
};
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
17
src/components/app.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
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>
|
||||
);
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
23
src/components/filter-input.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
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;
|
||||
};
|
||||
66
src/components/modal.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
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;
|
||||
};
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
29
src/components/sort-select.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
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;
|
||||
};
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
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);
|
||||
29
src/containers/album-list.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
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);
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
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);
|
||||
14
src/containers/filter-input.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
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);
|
||||
14
src/containers/modal.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
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);
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
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);
|
||||
14
src/containers/sort-select.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
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
|
|
@ -1,32 +0,0 @@
|
|||
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' }});
|
||||
31
src/index.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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" } });
|
||||
17
src/interfaces/index.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
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;
|
||||
}
|
||||
};
|
||||
19
src/reducers/albums.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
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;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import albums from './albums';
|
||||
import visibilityFilter from './visibility-filter';
|
||||
import sortKey from "./sort-key";
|
||||
|
||||
export default combineReducers({
|
||||
albums,
|
||||
visibilityFilter,
|
||||
sortKey,
|
||||
});
|
||||
12
src/reducers/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
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
|
||||
});
|
||||
20
src/reducers/selected-album.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
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;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
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;
|
||||
}
|
||||
};
|
||||
17
src/reducers/sort-key.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
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;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { SET_VISIBILITY_FILTER } from '../actions';
|
||||
|
||||
export default (state = '', action) => {
|
||||
switch (action.type) {
|
||||
case SET_VISIBILITY_FILTER:
|
||||
return action.payload.filter;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
17
src/reducers/visibility-filter.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
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;
|
||||
}
|
||||
};
|
||||
32
src/sagas/index.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
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;
|
||||
};
|
||||
}
|
||||
68
static/dummy-data/albums.json
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
[
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
]
|
||||
|
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 |
10
tsconfig.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"~*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
};
|
||||