Compare commits

...

24 commits
v1.1 ... main

Author SHA1 Message Date
45a6ea25c8 Upgrade NPM dependencies 2021-06-27 12:26:03 +02:00
da4f93a30b Finish removal of version controlled content
From now on, use README instructions to add example dummy data.
2021-06-27 12:25:14 +02:00
bff9b2fdd7 Remove content from version control
albums.json and the covers are now handled as symbolic links. Some minor
changes are done in the tsx files as well to mimic some adjustments in
the data.
2021-04-25 09:29:14 +02:00
Anders Ytterström
ea6a48deef Add 2021 progress 2021-03-31 10:06:31 +02:00
Anders Ytterström
e0a39dd4d8 Add King Diamond - "Them" 2020-09-14 13:28:26 +02:00
dependabot[bot]
ecaa1271bf Bump acorn from 5.7.3 to 5.7.4
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-04-17 12:06:55 +02:00
3839813a0a
Update README.md 2020-03-12 15:37:56 +01:00
Anders Ytterström
f5f5d61413
Add youtube video to README 2020-03-12 15:36:01 +01:00
9bcf0ecc56 Make modal great again 2020-03-05 21:48:45 +01:00
755cf14828 Add License 2020-03-05 20:19:48 +01:00
4e28567959 Update README 2020-03-05 20:17:45 +01:00
Anders Ytterström
6ba958fafa Replace interfaces with Types 2020-03-05 20:14:09 +01:00
Anders Ytterström
cb9f3ec3d1
Convert app to TypeScript 2020-03-05 08:07:46 +01:00
Anders Ytterström
39f0542ef2
Replace Webpack with Parcel
Change path to some files to ease the no-config static file management
2020-03-04 19:35:29 +01:00
Anders Ytterström
93358ac607
Improve design responsiveness 2019-12-28 13:18:52 +01:00
e04e04bee9 Update dependencies
curse you babel 7, webpack and all other npm crap.
2019-09-17 21:49:19 +02:00
Anders Ytterström
055511e555 Lint code and introduce BEM 2019-04-24 09:58:02 +02:00
Anders Ytterström
d447143312 Display selected album in Modal
A new Component and Container is introduced: Modal.

It dispatches an UNSELECT_ALBUM action when clicked on.

Some CSS is added to bur out the background and display the modal
content properly.
2019-04-24 09:58:02 +02:00
Anders Ytterström
1ceb248df9 Handle click on album
Click on album should dispatch an SELECT_ALBUM action.

This is done by map to AlbumList, and providing a property to each
album. It could also had been done by introducing a container for Album,
but IMHO that's not motivated.
2019-04-24 09:58:02 +02:00
Anders Ytterström
60e70f4b34 Add selected album actions and reducers
Default state is an empty object ({}).
SELECT_ALBUM will update state with an album.
UNSELECT_ALBUM resets state to default state.
2019-04-24 09:58:02 +02:00
Anders Ytterström
c11b1d4653 Load albums asyncronously
Introducing redux-sagas.
2019-04-24 09:58:02 +02:00
Anders Ytterström
3f0ac404f0 Do some spring cleaning 2019-04-24 09:58:02 +02:00
Anders Ytterström
4266dbcf6b Upgrade dependencies 2019-04-24 09:58:02 +02:00
Anders Ytterström
d722027781 🎨 Improve code readability
Rename files for consistency and clarification
2019-04-24 09:57:41 +02:00
65 changed files with 22298 additions and 4360 deletions

View file

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

15
.editorconfig Normal file
View file

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

216
.eslintrc.js Normal file
View file

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

9
.gitignore vendored
View file

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

35
.stylelintrc.js Normal file
View file

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

1
.tool-versions Normal file
View file

@ -0,0 +1 @@
nodejs 12.10.0

8
LICENSE Normal file
View 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
* ----------------------------------------------------------------------------
*/

34
README.md Normal file
View file

@ -0,0 +1,34 @@
# 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 443 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View file

@ -1,98 +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;
}
figure img {
width: 100px;
height: 100px;
padding: 5px;
display: block;
border: 1px solid #a83;
}

263
css/brutal.css Normal file
View 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 === */

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="/assets/style.css">
<title>Brütal Legend</title>
<link rel="stylesheet" href="./css/brutal.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="/bundle.js"></script>
<script src="./src/index.tsx"></script>
</html>

24904
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,29 +1,38 @@
{
"name": "redux-simple-starter",
"version": "1.0.0",
"description": "Simple starter package for Redux with React and Babel support",
"main": "index.js",
"repository": "git@github.com:StephenGrider/ReduxSimpleStarter.git",
"scripts": {
"start": "node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js",
"build": "webpack -p --define process.env.NODE_ENV='\"production\"' --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-dev-server": "^1.14.0"
},
"dependencies": {
"babel-preset-stage-1": "^6.1.18",
"lodash": "^3.10.1",
"react": "16.3.2",
"react-dom": "16.3.2",
"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"
]
}

View file

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

40
src/actions/index.ts Normal file
View 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
});

View file

@ -1,20 +0,0 @@
import React, { Component } from 'react';
import Album from "./album";
export default class AlbumList extends Component {
componentWillMount() {
this.props.loadAlbumData();
}
render() {
const {
albums,
} = this.props;
return (
<div id="albums">
{albums.map(album => (
<Album key={album.id} {...album} />
))}
</div>
);
}
}

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

View file

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

View file

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

17
src/components/app.tsx Normal file
View 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>
);

View file

@ -1,17 +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>
);
}
}

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

View file

@ -1,20 +0,0 @@
import React, { Component } from 'react';
export default class SortSelect extends Component {
render() {
const { value, handleOnChange } = this.props;
return (
<div>
<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

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

View file

@ -1,24 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import AlbumList from '../components/album-list';
import { initAlbumList } from "../actions";
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),
sortKey: state.sortKey,
});
const mapDispatchToProps = (dispatch) => ({
loadAlbumData: () => dispatch(initAlbumList())
});
export default connect(mapStateToProps, mapDispatchToProps)(AlbumList);

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

View file

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

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

View file

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

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

View file

@ -1,18 +0,0 @@
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')
);

31
src/index.tsx Normal file
View 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
View 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;
}

View file

@ -1,188 +0,0 @@
import {INIT_ALBUM_LIST} from '../actions';
export default (state = [], action) => {
switch (action.type) {
case INIT_ALBUM_LIST:
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"
}];
default:
return state;
}
};

19
src/reducers/albums.ts Normal file
View 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;
}
};

View file

@ -1,10 +0,0 @@
import { combineReducers } from 'redux';
import albums from './albums';
import visibilityFilter from './visibilityFilter';
import sortKey from "./sortKey";
export default combineReducers({
albums,
visibilityFilter,
sortKey,
});

12
src/reducers/index.ts Normal file
View 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
});

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

17
src/reducers/sort-key.ts Normal file
View 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;
}
};

View file

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

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

View file

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

32
src/sagas/index.ts Normal file
View 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;
};
}

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

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

10
tsconfig.json Normal file
View file

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

View file

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