Compare commits

..

4 commits
main ... v1.0

Author SHA1 Message Date
Anders Ytterström
7677570ad2 Remove webpack-dev-server
Severe security problems, and it is not needed since it only is used locally.

Here, it is replaced by a watch script, and relative paths to assets and resources so that index.html can be viewed directly in the browser instead of via a dev server.

Also, some minor production preparing stuffs (hiding elements not yet ready) and some ugly white space inconsistecy fixes.
2019-02-10 11:52:51 +01:00
Anders Ytterström
79bbf9188c Upgrade dependencies 2018-11-27 13:47:28 +01:00
Anders Ytterström
38f4244caf 🎨 Rename files for consistency and clarification 2018-11-26 11:31:53 +01:00
Anders Ytterström
74732c1683 🎨 Improve code readability 2018-11-26 09:46:05 +01:00
65 changed files with 5502 additions and 22334 deletions

View file

@ -1,3 +1,3 @@
{
"presets": ["@babel/preset-env", "@babel/react"]
"presets": ["react", "es2015", "stage-1"]
}

View file

@ -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

View file

@ -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
View file

@ -6,12 +6,3 @@ npm-debug.log
# IntelliJ
*.iml
/.idea
.cache
_build
.vscode
docs
static/albums.json
static/covers

View file

@ -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
}
};

View file

@ -1 +0,0 @@
nodejs 12.10.0

View file

@ -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
* ----------------------------------------------------------------------------
*/

View file

@ -1,34 +0,0 @@
# BRÜTAL LEGEND
https://www.youtube.com/embed/VW88ofmfF0w
Progress visualisation of the quest to own a vinyl copy of all songs older than 1990 used in [Brütal Legend](https://en.wikipedia.org/wiki/Br%C3%BCtal_Legend), where possible.
Also a project for learning
- CSS Grid layout,
- React,
- Redux,
- 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

View file

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 271 KiB

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View file

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

View file

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View file

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

BIN
assets/covers/06.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

BIN
assets/covers/07.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
assets/covers/08.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
assets/covers/09.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/covers/10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
assets/covers/11.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

BIN
assets/covers/12.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
assets/covers/13.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
assets/covers/14.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
assets/covers/15.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

106
assets/style.css Normal file
View 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;
}

View file

@ -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 === */

View file

@ -1,13 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<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>
</head>
<body>
<div id="brutal"></div>
<footer>av <a href="http://madr.se" rel="author">madr</a> 2018</footer>
</body>
<script src="./src/index.tsx"></script>
<script src="bundle.js"></script>
</html>

26096
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,38 +1,28 @@
{
"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"
]
"name": "brutal-legend",
"version": "1.0.0",
"description": "React+Redux app",
"main": "index.js",
"repository": "git@github.com:StephenGrider/ReduxSimpleStarter.git",
"scripts": {
"build": "webpack -p --define process.env.NODE_ENV='\"production\"' --progress --colors",
"watch": "webpack -p --define process.env.NODE_ENV='\"production\"' --watch --progress --colors"
},
"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",
"webpack": "^1.12.9",
"webpack-cli": "^3.2.3"
},
"dependencies": {
"babel-preset-stage-1": "^6.1.18",
"react": "16.3.2",
"react-dom": ">=16.3.3",
"react-redux": "5.0.7",
"redux": "4.0.0"
}
}

16
src/actions/index.js Normal file
View file

@ -0,0 +1,16 @@
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
export const SET_SORT_KEY = 'SET_SORT_KEY';
export const setVisibilityFilter = filter => ({
type: SET_VISIBILITY_FILTER,
payload: {
filter
}
});
export const setSortKey = key => ({
type: SET_SORT_KEY,
payload: {
key,
}
});

View file

@ -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
});

View 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>
);
}
}

View file

@ -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
View 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>
)
}
}

View file

@ -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;
};

19
src/components/app.jsx Normal file
View file

@ -0,0 +1,19 @@
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>
<SortSelectContainer />
<FilterInputContainer />
</header>
<AlbumListContainer />
</div>
);
}
}

View file

@ -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>
);

View 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>
);
}
}

View file

@ -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;
};

View file

@ -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;
};

View 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>
);
}
}

View file

@ -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;
};

View 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);

View file

@ -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);

View 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);

View file

@ -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);

View file

@ -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);

View 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);

View file

@ -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);

18
src/index.js Normal file
View file

@ -0,0 +1,18 @@
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './reducers';
import App from './components/app';
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('brutal')
);

View file

@ -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" } });

View file

@ -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;
}

181
src/reducers/albums.js Normal file
View file

@ -0,0 +1,181 @@
export default () => {
return [{
"id": 0,
"purchased_on": "2016-01-24",
"artist": "Sanctuary",
"songs": [
"Battle Angels"
],
"title": "Refuge Denied",
"year": 1987,
"description": "Bandet bildades 1985 i Seattle. De hann med två plattor innan de upplöstes i början av 1990-talet (för att 18 år senare återförenas ...). De var gunstlingar hos Dave Mustaine, som axlade producentrollen på det här alldeles strålande debutalbumet.\n\nDet härliga ljudet av sent 1980-tal i kombination med höga falsetter som sopar banan med rätt många Power Metal-band som kom till scenen 20 år senare gör att mitt projekt får en bra start. Det episka omslaget är dessutom alldeles för bra för att komma till sin rätt på en läskärm eller ett CD-fodral.",
"img": "00.jpg"
},{
"id": 1,
"purchased_on": "2016-01-31",
"artist": "Nitro",
"songs": [
"Machine Gunn Eddie"
],
"title": "O.F.R.",
"year": 1989,
"description": "Bandet bildades 1987 i Hollywood och var aktiva fram till 1993. De gjorde bara två skivor, varav O.F.R. är deras debutalbum, delvis självproducerat.\n\nDe gick under etiketten \"Glam Metal\" och jag har svårt att själv bedöma huruvida detta är korrekt eller inte; jag uppskattar dock intensiteten, kreativiteten och mixen av grymtande, imponerande falsetter (det längsta falsettskriket är 32 sekunder!), dubbeltramp och de utflippade riffen.\n\nOmslaget är närmast att betrakta som \"utsökt så in i helvete\".",
"img": "01.jpg"
},{
"id": 2,
"purchased_on": "2016-03-12",
"artist": "Omen",
"songs": [
"Axeman"
],
"title": "Battle Cry",
"year": 1984,
"description": "Bandet bildades 1983 i Los Angeles och föll litet grann i glömska efter de fyra första skivorna 1984-1988, med bara sporadiska skivsläpp efter det.\n\nDe brukar räknas till en av förfäderna inom Power Metal och The Axeman, det första spåret på debutskivan \"Battle Cry\" är inget undantag, med intro i form av en viskande mansröst som övergår till tuggande riff med symfoniska inslag.\n\nOmslaget hade gjort Rhapsody (OF FIRE) stolta, och många fler avundsjuka.",
"img": "02.jpg"
},{
"id": 3,
"purchased_on": "2016-03-20",
"artist": "KISS",
"songs": [
"God of Thunder"
],
"title": "Destroyer",
"year": 1976,
"description": "KISS är ett band som nästan är omöjligt att missa, men likväl: de bildades 1973 i New York City och är mest kända för sina påkostade scenframträdanden och utsmyckade scenpersonligheter.\n\n\"Destroyer\" är den fjärde LPn och den har ett gäng betydligt mer kända låtar än God of Thunder. Jag har alltid uppskattat Brütal Legend för att man valt denna låt istället, då den passar oerhört bra in-game.\n\nOmslaget? Sjukt episkt, ett av de snyggaste KISS har i sin katalog i mitt tycke.",
"img": "03.jpg"
},{
"id": 4,
"purchased_on": "2015-01-01",
"artist": "Ozzy Osbourne",
"songs": [
"Mr. Crowley"
],
"title": "Bizzard of Ozz",
"year": 1980,
"description": "En riktig klassiker! Mr Crowley är den avslutande låten på Ozzys debutalbum som soloartist. Min relation till skivan präglas främst av att det från början var tänkt att vara bandet The Blizzard of Ozzs debutskiva, men ibland ryter management (läs: Fru Ozzbourne ... ) ifrån.\n\nJag gillar att man valde att ta med Mr Crowley, då denna låt mer än någon annan på debutskivan uppvisar vad Randy Rhoads var kapabel till och vilken ribba han satte för framtida gitarrister för Ozzy. RIP. I spelet används låten högst förträffligt i main plot.\n\nFör en gångs skull är det ett omslag som är högst tvivelaktigt. Jag kan sträcka mig till att kalla det \"fulsnyggt\" ...",
"img": "04.jpg"
},{
"id": 5,
"purchased_on": "2015-01-01",
"artist": "Running Wild",
"songs": [
"Riding the Storm"
],
"title": "Death or Glory",
"year": 1989,
"description": "Running Wild bildades 1976 och hade 2-3 skivor med hedniska teman innan de övergick till sitt kännetecken: piratflaggor, nitar och speed metal, för att senare på 90-talet dra åt power metal-hållet. De är ett av de viktigaste banden under min uppväxt.\n\n\"Death or Glory\" är en av deras mest kända skivor, och Riding the storm öppnar den skivan med ett härligt intro och exemplariska riff.\n\nOmslaget är supernice.",
"img": "05.jpg"
},{
"id": 6,
"purchased_on": "2016-07-09",
"artist": "Diamond Head",
"songs": [
"Am I Evil"
],
"title": "Am I Evil",
"year": 1987,
"description": "Diamond Head bildades 1976 i England och anses i efterhand vara en av pionärerna inom NWOBHM. De släppte ett gäng skivor och splittrades för första gången 1985, för att sedan återförenas och splittras igen några gånger. Det är under den första tiden av inaktivitet som samlingsalbumet \"Am I Evil\" släpptes.\n\n\"Am I Evil\" är en låt från debutskivan \"Lightning to the Nations\" (1980) men har spelats in i flera versioner, där Metallicas cover troligtvis är den som är den mest kända. I Brütal Legend användes den version som släpptes på samlingsalbumet, vilket är en nyinspelad version av bandet själva. Personligen är detta min favorit av de versioner jag lyssnat på.\n\nOmslaget är så fint att det knappt ens går att beskriva. Färgsättningen, motiven, bandlogon ... ren hårdrockskärlek! Det är den här typen av omslag som var inspiration till hur Brütal Legends \"Age of Metal\" designades.",
"img": "06.jpg"
},{
"id": 7,
"purchased_on": "2016-07-10",
"artist": "Anvil",
"songs": [
"March of the Crabs",
"Tag Team"
],
"title": "Metal on Metal",
"year": 1982,
"description": "Anvil är ett kanadeniskt band, bildat 1978, som efter några bra år under tidigt 1980-tal föll i glömska. De fick förnyat intresse hos fans efter dokumentären \"Anvil! The Story of Anvil\" som producerades av en av bandets roadies.\n\nDet här är den första av flera skivor jag skriver om som har mer än en låt i spelet Brütal Legend, och det är välförjänt. Detta är med klassisk Heavy Metal-mått mätt en skiva som åldrats oerhört väl. Sång, riff, gitarrsolon och produktion är fläckfri.\n\nOmslaget är charmigt och tidsenligt, med en sväng av humor.",
"img": "07.jpg"
},{
"id": 8,
"purchased_on": "2016-07-29",
"artist": "Black Sabbath",
"songs": [
"Children of the Grave"
],
"title": "Masters of Reality",
"year": 1971,
"description": "Den första och ej sista låten med Black Sabbath, detta fantastiskt inflytelserika band som hade sin klassiska period mellan 1970 och 1975. \"Masters of Reality\" är den tredje skivan släppt inom loppet av två år, och är den första där bandet spelar nedstämt.\n\nChildren of the Grave är en odödlig Black Sabbath-låt och passar utmärkt i Brütal Legend, med dess tuggande bass, hårda trummor och primitivt distade gitarriff.",
"img": "08.jpg"
},{
"id": 9,
"purchased_on": "2016-10-30",
"artist": "Budgie",
"songs": [
"Breadfan"
],
"title": "Never Turn Your Back on a Friend",
"year": 1973,
"description": "Rockbandet Budgie bildades 1967 i Cardiff, Wales och räknas till ett av de första Heavy Metal-banden. De spelade tillsammans i 20 år och har sedan återförenats temporärt ett par gånger.\n\nLåten \"Breadfan\" är förmodligen mest känd för att Metallica gjort en cover på den.\n\nOmslaget är otroligt snyggt!",
"img": "09.jpg"
},{
"id": 10,
"purchased_on": "2016-12-28",
"artist": "Savatage",
"songs": [
"Hall of the Mountain King"
],
"title": "Hall of the Mountain King",
"year": 1987,
"description": "Savatage bildades 1978 och hade inledningsvis sval framgång. Denna skiva markerade ett skifte i musikalisk inriktning och är också den första av fyra skivsläpp som anses vara bandets bästa period. Nämnda period avslutades dessvärre på grund av att en grundande medlem dödades i en trafikolycka 1993.\n\nTitelspåret är episk Heavy Metal, har progressiva inslag och är full av energi. Omslaget är så bra som något omslag kunde vara med allt på elvan under 1980-talet.",
"img": "10.jpg"
},{
"id": 11,
"purchased_on": "2017-01-03",
"artist": "Metal Church",
"songs": [
"Metal Church"
],
"title": "Metal Church",
"year": 1984,
"description": "Mer representation av amerikanskt 1980-tal. Metal Church kombinerar Heavy Metal med Speed Metal, och redan på denna debutplatta finns mycket element som 20 år senare skulle bli typiska för Power Metal. Det sägs att Metallica gick i god för Metal Church hos Elekta, det skivbolag som gav ut debuten. Bandet lades på is för första gången 1994, men kom alltid tillbaka och räknas idag som aktivt, med 11 skivor släppta.\n\nOmslaget är helt otroligt jävla snyggt. Det är exakt den här typen av skivomslag som inspirerade Double Fines design av World of Metal.",
"img": "11.jpg"
},{
"id": 12,
"purchased_on": "2017-01-25",
"artist": "Ostrogoth",
"songs": [
"Queen of Desire"
],
"title": "Ecstasy and Anger",
"year": 1984,
"description": "Ostrogoth var ett NWOBHM-band från Ghent, Belgien som var aktivt 1980-1988, för att sedan lägga ner efter tre skivor. Återföreningar har skett men inte med bandets originalmedlemmar.\n\nDetta är ett band som lånat från andra mer framgångsrika band, men de gör det snyggt och bra. Låten ifråga har ett Van Halen-inspirerat intro och en stark refräng, varvat med det sedvanlig gitarr-onani.\n\nOmslaget, med en guld- och silverfärgad skorpion, har sin charm.",
"img": "12.jpg"
},{
"id": 13,
"purchased_on": "2017-05-18",
"artist": "Coroner",
"songs": [
"Skeleton on Your Shoulder"
],
"title": "Punishment for Decadence",
"year": 1988,
"description": "Coroner är ett experimentiellt Thrash Metal-band från Zurich, Schweiz som lade ner 1996 utan att riktigt nått någon publik utanför Europa, för att återförenas 2011. Bandmedlemmarna hade lärt känna varandra som roadies till Celtic Frost.\n\nOmslaget är inte så jättespännande men det beskriver låten riktigt bra.",
"img": "13.jpg"
},{
"id": 14,
"purchased_on": "2017-07-27",
"artist": "Riot",
"songs": [
"Narita",
"Road Racin'"
],
"title": "Narita",
"year": 1979,
"description": "Heavy Metal-bandet Riot bildades i New York City 1975 och är sedan 2012 känd som Riot V. Bandet har haft två avbrott och hade sin storhetstid i början av 1980-talet, från vilken ingen originalmedlem består.\n\nNarita är bandets andra skiva och har två låtar med i spelet, båda relaterade till Ironheade: klassisk old school Heavy Metal med 1970-talets mjuka hörn.\n\nOmslaget föreställer en Sumobrottare i spindelmannen-spandex och illerhuvud som dansar över ett fält av människokranium, hållandes en stridsyxa. I bakgrunden syns ett snötäckt fjäll och ett anländande flygplan.",
"img": "14.jpg"
},{
"id": 15,
"purchased_on": "2017-07-28",
"artist": "Motörhead",
"songs": [
"(We are) The Roadcrew"
],
"title": "Ace of Spades",
"year": 1982,
"description": "Motörhead bildades 1975 och var aktivt fram tills att dess frontman Lemmy Kilmister, en sann kultklenod inom hårdrocken, gick bort 2015. Det är ingen tillfällighet att så många låtar av Motörhead är med i ett spel som Brütal Legend.\n\n\"Ace of Spades\" är den fjärde skivan och inledde Motörheads 1980-tal. Jag förknippade den tidigare med dess titelspår, men (We Are) The Roadcrew, en låt som skrevs för att särskilt hylla bandets roadies, är mycket passande till spelets berättelse.\n\nDet tidlösa omslaget är någon sorts variant av spaghetti-western med skinnställ.",
"img": "15.jpg"
}];
};

View file

@ -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
View 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,
});

View file

@ -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
});

View file

@ -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
View 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;
}
};

View file

@ -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;
}
};

View 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;
}
};

View file

@ -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;
}
};

View file

@ -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;
};
}

View file

@ -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"]
}
]

View file

@ -1,10 +0,0 @@
{
"compilerOptions": {
"jsx": "react",
"baseUrl": "./src",
"paths": {
"~*": ["./*"]
}
},
"include": ["src/**/*"]
}

30
webpack.config.js Normal file
View 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
}
}
};