Convert app to TypeScript
This commit is contained in:
parent
39f0542ef2
commit
cb9f3ec3d1
42 changed files with 485 additions and 418 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,3 +9,4 @@ npm-debug.log
|
||||||
|
|
||||||
.cache
|
.cache
|
||||||
_build
|
_build
|
||||||
|
.vscode
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,5 @@
|
||||||
<body>
|
<body>
|
||||||
<div id="brutal"></div>
|
<div id="brutal"></div>
|
||||||
</body>
|
</body>
|
||||||
<script src="./src/index.js"></script>
|
<script src="./src/index.tsx"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -8758,6 +8758,12 @@
|
||||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
|
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"typescript": {
|
||||||
|
"version": "3.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz",
|
||||||
|
"integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"typescript-compare": {
|
"typescript-compare": {
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz",
|
||||||
|
|
|
||||||
69
package.json
69
package.json
|
|
@ -1,36 +1,37 @@
|
||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env NODE_ENV=development parcel index.html --public-url / --out-dir _build",
|
"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 /urban-enigma/ --out-dir docs --no-source-maps --no-content-hash",
|
"build": "cross-env NODE_ENV=production parcel build index.html --public-url /urban-enigma/ --out-dir docs --no-source-maps --no-content-hash",
|
||||||
"lint": "cross-env NODE_ENV=development prettier --check src/**/* assets/*.css"
|
"lint": "cross-env NODE_ENV=development prettier --check src/**/*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.4.0",
|
"@babel/core": "^7.4.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.4.0",
|
"@babel/plugin-transform-runtime": "^7.4.0",
|
||||||
"@babel/preset-env": "^7.5.5",
|
"@babel/preset-env": "^7.5.5",
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"autoprefixer": "^9.5.0",
|
"autoprefixer": "^9.5.0",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"jest": "^24.9.0",
|
"jest": "^24.9.0",
|
||||||
"parcel-bundler": "^1.12.3",
|
"parcel-bundler": "^1.12.3",
|
||||||
"parcel-plugin-static-files-copy": "^2.3.1",
|
"parcel-plugin-static-files-copy": "^2.3.1",
|
||||||
"prettier": "^1.18.2"
|
"prettier": "^1.18.2",
|
||||||
},
|
"typescript": "^3.6.3"
|
||||||
"dependencies": {
|
},
|
||||||
"@babel/runtime-corejs2": "^7.4.2",
|
"dependencies": {
|
||||||
"react": "^16.8.5",
|
"@babel/runtime-corejs2": "^7.4.2",
|
||||||
"react-dom": "^16.8.5",
|
"react": "^16.8.5",
|
||||||
"react-redux": "^6.0.1",
|
"react-dom": "^16.8.5",
|
||||||
"redux": "^4.0.1",
|
"react-redux": "^6.0.1",
|
||||||
"redux-saga": "^1.0.2"
|
"redux": "^4.0.1",
|
||||||
},
|
"redux-saga": "^1.0.2"
|
||||||
"postcss": {
|
},
|
||||||
"modules": false,
|
"postcss": {
|
||||||
"plugins": {
|
"modules": false,
|
||||||
"autoprefixer": {}
|
"plugins": {
|
||||||
}
|
"autoprefixer": {}
|
||||||
},
|
}
|
||||||
"browserslist": [
|
},
|
||||||
"defaults"
|
"browserslist": [
|
||||||
]
|
"defaults"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
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 => ({
|
|
||||||
type: SET_VISIBILITY_FILTER,
|
|
||||||
payload: {
|
|
||||||
filter
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setSortKey = key => ({
|
|
||||||
type: SET_SORT_KEY,
|
|
||||||
payload: {
|
|
||||||
key,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const albumsLoadedOk = albums => ({
|
|
||||||
type: LOAD_ALBUMS_OK,
|
|
||||||
payload: {
|
|
||||||
albums,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const selectAlbum = album => ({
|
|
||||||
type: SELECT_ALBUM,
|
|
||||||
payload: {
|
|
||||||
album
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const unselectAlbum = () => ({
|
|
||||||
type: UNSELECT_ALBUM,
|
|
||||||
});
|
|
||||||
40
src/actions/index.ts
Normal file
40
src/actions/index.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { Album } from "../interfaces";
|
||||||
|
|
||||||
|
export const LOAD_ALBUMS = "LOAD_ALBUMS";
|
||||||
|
export const LOAD_ALBUMS_OK = "LOAD_ALBUMS_OK";
|
||||||
|
export const SELECT_ALBUM = "SELECT_ALBUM";
|
||||||
|
export const SET_VISIBILITY_FILTER = "SET_VISIBILITY_FILTER";
|
||||||
|
export const SET_SORT_KEY = "SET_SORT_KEY";
|
||||||
|
export const UNSELECT_ALBUM = "UNSELECT_ALBUM";
|
||||||
|
|
||||||
|
export const setVisibilityFilter = (filter: string) => ({
|
||||||
|
type: SET_VISIBILITY_FILTER,
|
||||||
|
payload: {
|
||||||
|
filter
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setSortKey = (key: string) => ({
|
||||||
|
type: SET_SORT_KEY,
|
||||||
|
payload: {
|
||||||
|
key
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const albumsLoadedOk = (albums: Array<Album>) => ({
|
||||||
|
type: LOAD_ALBUMS_OK,
|
||||||
|
payload: {
|
||||||
|
albums
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectAlbum = (album: Album) => ({
|
||||||
|
type: SELECT_ALBUM,
|
||||||
|
payload: {
|
||||||
|
album
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const unselectAlbum = () => ({
|
||||||
|
type: UNSELECT_ALBUM
|
||||||
|
});
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Album from './album';
|
|
||||||
|
|
||||||
export default class AlbumList extends Component {
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
albums,
|
|
||||||
handleOnClick,
|
|
||||||
blurred,
|
|
||||||
} = this.props;
|
|
||||||
const classNames = blurred ? 'blur' : ''
|
|
||||||
return (
|
|
||||||
<div className={"albums " + classNames}>
|
|
||||||
{albums.map(album => (
|
|
||||||
<Album
|
|
||||||
key={album.id}
|
|
||||||
album={album}
|
|
||||||
handleOnClick={handleOnClick}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
25
src/components/album-list.tsx
Normal file
25
src/components/album-list.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from "react";
|
||||||
|
import Album from "./album";
|
||||||
|
import * as interfaces from "../interfaces";
|
||||||
|
|
||||||
|
export default (props: AlbumList) => {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AlbumList {
|
||||||
|
albums: Array<interfaces.Album>;
|
||||||
|
handleOnClick: Function;
|
||||||
|
blurred: boolean;
|
||||||
|
}
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
export default class Album extends Component {
|
|
||||||
handleKeyPress(e, callback) {
|
|
||||||
const SPACE_KEY = 32
|
|
||||||
const ENTER_KEY = 13
|
|
||||||
if (e.charCode === SPACE_KEY || e.charCode === ENTER_KEY) {
|
|
||||||
e.preventDefault();
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { album, handleOnClick } = this.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>
|
|
||||||
#{id+1}: {artist} - {song}, från "{title}" ({year})<br />
|
|
||||||
<small>✔️ {purchased_on}</small>
|
|
||||||
</span>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
43
src/components/album.tsx
Normal file
43
src/components/album.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React from "react";
|
||||||
|
import * as interfaces from "../interfaces";
|
||||||
|
|
||||||
|
export default (props: Album) => {
|
||||||
|
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>
|
||||||
|
#{id + 1}: {artist} - {song}, från "{title}" ({year})<br />
|
||||||
|
<small>✔️ {purchased_on}</small>
|
||||||
|
</span>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Album {
|
||||||
|
album: interfaces.Album;
|
||||||
|
handleOnClick: Function;
|
||||||
|
}
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import AlbumList from '../containers/album-list';
|
|
||||||
import FilterInput from '../containers/filter-input';
|
|
||||||
import Modal from '../containers/modal';
|
|
||||||
|
|
||||||
export default class App extends Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<header>
|
|
||||||
<h1>Brütal Legend</h1>
|
|
||||||
<FilterInput />
|
|
||||||
</header>
|
|
||||||
<AlbumList />
|
|
||||||
<Modal />
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
15
src/components/app.tsx
Normal file
15
src/components/app.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import React from "react";
|
||||||
|
import AlbumList from "../containers/album-list";
|
||||||
|
import FilterInput from "../containers/filter-input";
|
||||||
|
import Modal from "../containers/modal";
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<React.Fragment>
|
||||||
|
<header>
|
||||||
|
<h1>Brütal Legend</h1>
|
||||||
|
<FilterInput />
|
||||||
|
</header>
|
||||||
|
<AlbumList />
|
||||||
|
<Modal />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
export default class FilterInput extends Component {
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
handleOnChange,
|
|
||||||
} = this.props;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
value={value}
|
|
||||||
onChange={evt => handleOnChange(evt.target.value)}
|
|
||||||
placeholder='Filtrera på år, artist, låt, skivtitel ...'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
20
src/components/filter-input.tsx
Normal file
20
src/components/filter-input.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default (props: FilterInput) => {
|
||||||
|
const { value, handleOnChange } = props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={evt => handleOnChange(evt.target.value)}
|
||||||
|
placeholder="Filtrera på år, artist, låt, skivtitel ..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FilterInput {
|
||||||
|
value: string;
|
||||||
|
handleOnChange: Function;
|
||||||
|
}
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
export default class Modal extends Component {
|
|
||||||
handleKeyPress(e, callback) {
|
|
||||||
alert("sdsdsd")
|
|
||||||
console.log(e.charCode)
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
artist,
|
|
||||||
title,
|
|
||||||
songs,
|
|
||||||
year,
|
|
||||||
img,
|
|
||||||
description,
|
|
||||||
handleOnClick,
|
|
||||||
} = this.props;
|
|
||||||
if (id === undefined) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const imagePath = `assets/covers/${img}`;
|
|
||||||
const song = songs.join(', ');
|
|
||||||
return (
|
|
||||||
<div className="selected-album"
|
|
||||||
tabIndex="0"
|
|
||||||
onClick={handleOnClick}
|
|
||||||
onKeyPress={e => this.handleKeyPress(e, handleOnClick)}>
|
|
||||||
<div className="selected-album__inner">
|
|
||||||
<span className="selected-album__summary">
|
|
||||||
#{id+1}: {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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
42
src/components/modal.tsx
Normal file
42
src/components/modal.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Album } from "../interfaces";
|
||||||
|
|
||||||
|
interface Modal {
|
||||||
|
album: Album;
|
||||||
|
handleOnClick: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props: Modal) => {
|
||||||
|
const handleKeyPress = (e: KeyboardEvent, callback: Function) => {
|
||||||
|
console.log(e.charCode);
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { album, handleOnClick } = props;
|
||||||
|
const { id, artist, title, songs, year, img, description } = album;
|
||||||
|
if (id === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
// const imagePath = `assets/covers/${img}`;
|
||||||
|
const song = songs.join(", ");
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="selected-album"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => handleOnClick()}
|
||||||
|
onKeyPress={e => handleKeyPress(e, handleOnClick)}
|
||||||
|
>
|
||||||
|
<div className="selected-album__inner">
|
||||||
|
<span className="selected-album__summary">
|
||||||
|
#{id + 1}: {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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
export default class SortSelect extends Component {
|
|
||||||
render() {
|
|
||||||
const { value, handleOnChange } = this.props;
|
|
||||||
return (
|
|
||||||
<div hidden>
|
|
||||||
<label htmlFor="sortBy">Sortera efter</label>
|
|
||||||
<select
|
|
||||||
id="sortBy"
|
|
||||||
value={value}
|
|
||||||
onChange={evt => handleOnChange(evt.target.value)}>
|
|
||||||
<option value="id">Inköpsdatum</option>
|
|
||||||
<option value="artist">Artist</option>
|
|
||||||
<option value="year">År</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
src/components/sort-select.tsx
Normal file
24
src/components/sort-select.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default (props: SortSelect) => {
|
||||||
|
const { value, handleOnChange } = 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SortSelect {
|
||||||
|
value: string;
|
||||||
|
handleOnChange: Function;
|
||||||
|
}
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import AlbumList from '../components/album-list';
|
|
||||||
import { selectAlbum } 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),
|
|
||||||
blurred: "id" in state.selectedAlbum,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
handleOnClick: album => dispatch(selectAlbum(album)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(AlbumList);
|
|
||||||
26
src/containers/album-list.ts
Normal file
26
src/containers/album-list.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
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) => {
|
||||||
|
if (filter) {
|
||||||
|
const term = filter.toLowerCase();
|
||||||
|
return albums.filter((album: Album) => atos(album).match(term));
|
||||||
|
}
|
||||||
|
return albums;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: State) => ({
|
||||||
|
albums: getAlbums(state.albums, state.visibilityFilter),
|
||||||
|
blurred: "id" in state.selectedAlbum
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: Function) => ({
|
||||||
|
handleOnClick: (album: Album) => dispatch(selectAlbum(album))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(AlbumList);
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import FilterInput from '../components/filter-input';
|
|
||||||
import { setVisibilityFilter } from '../actions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
value: state.visibilityFilter,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
handleOnChange: filter => dispatch(setVisibilityFilter(filter)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(FilterInput);
|
|
||||||
14
src/containers/filter-input.ts
Normal file
14
src/containers/filter-input.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import FilterInput from "../components/filter-input";
|
||||||
|
import { setVisibilityFilter } from "../actions";
|
||||||
|
import { State } from "../interfaces";
|
||||||
|
|
||||||
|
const mapStateToProps = (state: State) => ({
|
||||||
|
value: state.visibilityFilter
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: Function) => ({
|
||||||
|
handleOnChange: (filter: string) => dispatch(setVisibilityFilter(filter))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(FilterInput);
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import Modal from '../components/modal';
|
|
||||||
import { unselectAlbum } from '../actions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
...state.selectedAlbum,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
handleOnClick: album => dispatch(unselectAlbum(album)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Modal);
|
|
||||||
15
src/containers/modal.ts
Normal file
15
src/containers/modal.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import React from "react";
|
||||||
|
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) => ({
|
||||||
|
handleOnClick: () => dispatch(unselectAlbum())
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Modal);
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import SortSelect from '../components/sort-select';
|
|
||||||
import {setSortKey} from "../actions";
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
value: state.sortKey
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
handleOnChange: (key) => dispatch(setSortKey(key))
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(SortSelect);
|
|
||||||
15
src/containers/sort-select.ts
Normal file
15
src/containers/sort-select.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import SortSelect from "../components/sort-select";
|
||||||
|
import { setSortKey } from "../actions";
|
||||||
|
import { State } from "../interfaces";
|
||||||
|
|
||||||
|
const mapStateToProps = (state: State) => ({
|
||||||
|
value: state.sortKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: Function) => ({
|
||||||
|
handleOnChange: (key: string) => dispatch(setSortKey(key))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(SortSelect);
|
||||||
32
src/index.js
32
src/index.js
|
|
@ -1,32 +0,0 @@
|
||||||
import '@babel/polyfill';
|
|
||||||
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';
|
|
||||||
|
|
||||||
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' }});
|
|
||||||
31
src/index.tsx
Normal file
31
src/index.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import React from "react";
|
||||||
|
import { render } from "react-dom";
|
||||||
|
import { createStore, applyMiddleware, compose } from "redux";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import createSagaMiddleware from "redux-saga";
|
||||||
|
import rootReducer from "./reducers";
|
||||||
|
import rootSagas from "./sagas";
|
||||||
|
import App from "./components/app";
|
||||||
|
import { LOAD_ALBUMS } from "./actions";
|
||||||
|
|
||||||
|
const sagaMiddleware = createSagaMiddleware();
|
||||||
|
|
||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
const store = createStore(
|
||||||
|
rootReducer,
|
||||||
|
composeEnhancers(applyMiddleware(sagaMiddleware))
|
||||||
|
);
|
||||||
|
|
||||||
|
sagaMiddleware.run(rootSagas);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById("brutal")
|
||||||
|
);
|
||||||
|
|
||||||
|
store.dispatch({ type: LOAD_ALBUMS, payload: { source: "./albums.json" } });
|
||||||
17
src/interfaces/index.ts
Normal file
17
src/interfaces/index.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
export interface Album {
|
||||||
|
id: number;
|
||||||
|
artist: string;
|
||||||
|
title: string;
|
||||||
|
songs: Array<string>;
|
||||||
|
year: number;
|
||||||
|
img: string;
|
||||||
|
purchased_on: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
albums: Array<Album>;
|
||||||
|
selectedAlbum: Album;
|
||||||
|
visibilityFilter: string;
|
||||||
|
sortKey: string;
|
||||||
|
}
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { LOAD_ALBUMS_OK } from '../actions';
|
|
||||||
|
|
||||||
export default (state = [], action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case LOAD_ALBUMS_OK:
|
|
||||||
const { albums } = action.payload;
|
|
||||||
return albums;
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
19
src/reducers/albums.ts
Normal file
19
src/reducers/albums.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { LOAD_ALBUMS_OK } from "../actions";
|
||||||
|
import { Album } from "../interfaces";
|
||||||
|
|
||||||
|
interface AlbumsAction {
|
||||||
|
type: string;
|
||||||
|
payload: {
|
||||||
|
albums: Array<Album>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (state: Array<Album> = [], action: AlbumsAction) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case LOAD_ALBUMS_OK:
|
||||||
|
const { albums } = action.payload;
|
||||||
|
return albums;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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,
|
|
||||||
});
|
|
||||||
0
src/reducers/index.ts
Normal file
0
src/reducers/index.ts
Normal file
|
|
@ -1,12 +0,0 @@
|
||||||
import { SELECT_ALBUM, UNSELECT_ALBUM } from '../actions';
|
|
||||||
|
|
||||||
export default (state = {}, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case SELECT_ALBUM:
|
|
||||||
return action.payload.album;
|
|
||||||
case UNSELECT_ALBUM:
|
|
||||||
return {};
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
20
src/reducers/selected-album.ts
Normal file
20
src/reducers/selected-album.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { SELECT_ALBUM, UNSELECT_ALBUM } from "../actions";
|
||||||
|
import { Album } from "../interfaces";
|
||||||
|
|
||||||
|
interface AlbumAction {
|
||||||
|
type: string;
|
||||||
|
payload: {
|
||||||
|
album: Album;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (state: Object = {}, action: AlbumAction) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SELECT_ALBUM:
|
||||||
|
return action.payload.album;
|
||||||
|
case UNSELECT_ALBUM:
|
||||||
|
return {};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { SET_SORT_KEY } from '../actions';
|
|
||||||
|
|
||||||
export default (state = 'id', action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case SET_SORT_KEY:
|
|
||||||
return action.payload.key;
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
17
src/reducers/sort-key.ts
Normal file
17
src/reducers/sort-key.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { SET_SORT_KEY } from "../actions";
|
||||||
|
|
||||||
|
interface SortKeyAction {
|
||||||
|
type: string;
|
||||||
|
payload: {
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (state: string = "id", action: SortKeyAction) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SET_SORT_KEY:
|
||||||
|
return action.payload.key;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { SET_VISIBILITY_FILTER } from '../actions';
|
|
||||||
|
|
||||||
export default (state = '', action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case SET_VISIBILITY_FILTER:
|
|
||||||
return action.payload.filter;
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
17
src/reducers/visibility-filter.ts
Normal file
17
src/reducers/visibility-filter.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { SET_VISIBILITY_FILTER } from "../actions";
|
||||||
|
|
||||||
|
interface VisibilityFilterAction {
|
||||||
|
type: string;
|
||||||
|
payload: {
|
||||||
|
filter: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (state: string = "", action: VisibilityFilterAction) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SET_VISIBILITY_FILTER:
|
||||||
|
return action.payload.filter;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,25 +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) {
|
|
||||||
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(),
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
32
src/sagas/index.ts
Normal file
32
src/sagas/index.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { put, takeEvery, all, call } from "redux-saga/effects";
|
||||||
|
import { LOAD_ALBUMS, albumsLoadedOk } from "../actions";
|
||||||
|
|
||||||
|
function* watchLoadAlbumsAsync() {
|
||||||
|
yield takeEvery(LOAD_ALBUMS, loadAlbumsAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
function* loadAlbumsAsync(action: LoadAlbumsAction) {
|
||||||
|
try {
|
||||||
|
const { source } = action.payload;
|
||||||
|
const data = yield call(
|
||||||
|
() =>
|
||||||
|
fetch(source)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => data),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
yield put(albumsLoadedOk(data));
|
||||||
|
} catch (error) {
|
||||||
|
yield console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function*() {
|
||||||
|
yield all([watchLoadAlbumsAsync()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoadAlbumsAction {
|
||||||
|
payload: {
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react",
|
||||||
|
"baseUrl": "./src",
|
||||||
|
"paths": {
|
||||||
|
"~*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue