diff --git a/.gitignore b/.gitignore index d66dc48..48c3a60 100644 --- a/.gitignore +++ b/.gitignore @@ -44,5 +44,4 @@ bower_components .node_repl_history # Compiled Files -dist/app.js -dist/style.css +dist diff --git a/dist/index.html b/dist/index.html index 4ab7321..6a19ea0 100644 --- a/dist/index.html +++ b/dist/index.html @@ -4,18 +4,14 @@ - Spotify - + Spotify Exercise - - + - + - -
- - + + - + \ No newline at end of file diff --git a/package.json b/package.json index 583ec5d..5a2c609 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "scripts": { "start": "node index.js", + "dev": "webpack-dev-server --open", "doc": "gulp doc", "build": "webpack", "test": "echo \"Error: no test specified\" && exit 1" @@ -25,6 +26,7 @@ "cors": "^2.8.1", "express": "^4.15.2", "helmet": "^3.5.0", + "material-design-icons": "^3.0.1", "md5": "^2.2.1", "mongoose": "^4.9.0", "normalize.css": "^5.0.0", @@ -34,7 +36,8 @@ "react-router": "^3.0.2", "react-router-redux": "^4.0.8", "redux": "^3.6.0", - "redux-saga": "^0.14.3" + "redux-saga": "^0.14.3", + "redux-thunk": "^2.2.0" }, "devDependencies": { "babel": "^6.23.0", diff --git a/src/API/searchApi.js b/src/API/searchApi.js new file mode 100644 index 0000000..dd6b7f5 --- /dev/null +++ b/src/API/searchApi.js @@ -0,0 +1,18 @@ +class SearchApi { + search(query) { + return new Promise((resolve, reject) => { + if(query.length > 3) { + fetch(`http://localhost:3000/search?q=${query}`) + .then(response => response.json()) + .then((data) => { + resolve(data.albums.items || []) + }, reject) + } + else { + resolve([]) + } + }) + } +} + +export default new SearchApi() diff --git a/src/Actions/Search.js b/src/Actions/Search.js new file mode 100644 index 0000000..c546cf6 --- /dev/null +++ b/src/Actions/Search.js @@ -0,0 +1,15 @@ +export const types = { + SEARCH_REQUESTED: 'SEARCH_REQUESTED', + SEARCH_RESOLVED: 'SEARCH_RESOLVED', + SEARCH_REJECTED: 'SEARCH_REJECTED' +} + +export const searchRequested = (q) => ({ + type: types.SEARCH_REQUESTED, + query: q +}) + +export const searchResolved = (data) => ({ + type: types.SEARCH_RESOLVED, + data +}) diff --git a/src/Components/ActionButton/ActionButton.js b/src/Components/ActionButton/ActionButton.js new file mode 100644 index 0000000..9a4fbcd --- /dev/null +++ b/src/Components/ActionButton/ActionButton.js @@ -0,0 +1,23 @@ +import React from 'react' + +import './ActionButton.scss' + +class ActionButton extends React.Component { + static propTypes = { + customClass: React.PropTypes.string, + label: React.PropTypes.string.isRequired, + onButtonClick: React.PropTypes.func.isRequired + } + + render() { + return( + + ) + } +} + +export default ActionButton diff --git a/src/Components/ActionButton/ActionButton.scss b/src/Components/ActionButton/ActionButton.scss new file mode 100644 index 0000000..26c15e3 --- /dev/null +++ b/src/Components/ActionButton/ActionButton.scss @@ -0,0 +1,10 @@ +.action-button { + border-radius: 40px; + border: 1px solid #03bb4f; + background-color: rgba(30, 30, 30, 0.7); + color: #fff; + padding: 8px 20px; + font-size: 12px; + font-weight: 100; + cursor: pointer; +} diff --git a/src/Components/AlbumList/AlbumItem/AlbumItem.js b/src/Components/AlbumList/AlbumItem/AlbumItem.js new file mode 100644 index 0000000..2516534 --- /dev/null +++ b/src/Components/AlbumList/AlbumItem/AlbumItem.js @@ -0,0 +1,43 @@ +import React from 'react' + +import ActionButton from '~/Components/ActionButton/ActionButton' +import './AlbumItem.scss' + +class AlbumItem extends React.Component { + static propTypes = { + item: React.PropTypes.object.isRequired, + onCommentClick: React.PropTypes.func + } + + constructor() { + super() + this.onItemClick = this.onItemClick.bind(this) + } + + onItemClick() { + this.props.onCommentClick(this.props.item) + } + + render() { + let url = `url(${this.props.item.images[0].url})` + + return( +
+ +
+ +
+
+

{ this.props.item.artists[0].name }

+

{ this.props.item.name }

+
+
+ +
+
+
+ ) + } +} + +export default AlbumItem diff --git a/src/Components/AlbumList/AlbumItem/AlbumItem.scss b/src/Components/AlbumList/AlbumItem/AlbumItem.scss new file mode 100644 index 0000000..3e8454a --- /dev/null +++ b/src/Components/AlbumList/AlbumItem/AlbumItem.scss @@ -0,0 +1,70 @@ +.album-item { + width: 100%; + height: 180px; + color: #fff; + border: 1px solid #181818; + margin-top: 14px; + background-size: cover; + background-position: center; + + span { + background-image: linear-gradient(to left, rgba(40, 40, 40, 0.85), #282828); + width: 100%; + height: 100%; + display: block; + } + + h1 { + width: 500px; + height: 60px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 46px; + font-weight: normal; + margin: 0; + } + + h2 { + font-size: 12px; + font-weight: normal; + color: #a0a097; + margin: 0; + } + + .album-item-image { + float: left; + width: 180px; + height: 100%; + display: block; + img { + width: 180px; + } + } + + .album-item-info { + float: left; + display: block; + margin-top: 23px; + padding-left: 30px; + } + + .album-item-actions { + float: right; + height: 100%; + display: flex; + flex-direction: column-reverse; + + .action-button { + margin: 20px 30px; + } + } + + .album-item-backimg { + position: absolute; + top: 0; + left: 0; + z-index: -1; + } + +} diff --git a/src/Components/AlbumList/AlbumList.js b/src/Components/AlbumList/AlbumList.js new file mode 100644 index 0000000..91bd028 --- /dev/null +++ b/src/Components/AlbumList/AlbumList.js @@ -0,0 +1,42 @@ +import React from 'react' +import { connect } from 'react-redux' +import AlbumItem from './AlbumItem/AlbumItem' + +import './AlbumList.scss' + +class AlbumList extends React.Component { + static propTypes = { + albums: React.PropTypes.array.isRequired + } + + constructor() { + super() + this.gotoComment = this.gotoComment.bind(this) + } + + gotoComment() { + //console.log(`Se clickeó el comment del item: ${ item.name }`) + } + + render() { + const items = this.props.albums.map((albumItem) => + + ) + return( +
+ { items } +
+ ) + } +} + +const mapStateToProps = (state) => ({ + albums: state.Search.albums +}) + +const mapDispatchToProps = () => ({}) + +export default connect(mapStateToProps, mapDispatchToProps)(AlbumList) diff --git a/src/Components/AlbumList/AlbumList.scss b/src/Components/AlbumList/AlbumList.scss new file mode 100644 index 0000000..6d29fa6 --- /dev/null +++ b/src/Components/AlbumList/AlbumList.scss @@ -0,0 +1,10 @@ +.album-list { + width: 66%; + margin: 10px auto; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: flex-start; + align-content: flex-start; + align-items: stretch; +} diff --git a/src/Components/Main/Main.js b/src/Components/Main/Main.js new file mode 100644 index 0000000..4b20506 --- /dev/null +++ b/src/Components/Main/Main.js @@ -0,0 +1,17 @@ +import React from 'react' +import Search from '../Search/Search' +import AlbumList from '../AlbumList/AlbumList' +import './main.scss' + +export class Main extends React.Component { + render() { + return( +
+ + +
+ ) + } +} + +export default Main diff --git a/src/Components/Main/main.scss b/src/Components/Main/main.scss new file mode 100644 index 0000000..9248194 --- /dev/null +++ b/src/Components/Main/main.scss @@ -0,0 +1,4 @@ +.main { + width: 100%; + margin: 0 auto; +} diff --git a/src/Components/Search/Search.js b/src/Components/Search/Search.js new file mode 100644 index 0000000..b4b88ac --- /dev/null +++ b/src/Components/Search/Search.js @@ -0,0 +1,34 @@ +import React from 'react' +import { connect } from 'react-redux' +import * as actions from '../../Actions/Search' +//import 'material-design-icons/iconfont/material-icons.css' +import './_search.scss' + +class Search extends React.Component { + static propTypes = { + onSearch: React.PropTypes.func + } + + render() { + return( +
+ { /*search */ } + +
+ ) + } +} + +const mapStateToProps = () => ({}) + +const mapDispatchToProps = (dispatch) => ({ + onSearch(query) { + dispatch(actions.searchRequested(query.target.value)) + } +}) + +export default connect(mapStateToProps, mapDispatchToProps)(Search) diff --git a/src/Components/Search/_search.scss b/src/Components/Search/_search.scss new file mode 100644 index 0000000..8be63a8 --- /dev/null +++ b/src/Components/Search/_search.scss @@ -0,0 +1,16 @@ +.searchbox { + font-size: 12px; + background-color: #282828; + padding: 12px; + .search-input { + color: #353535; + padding: 0 24px; + border: 1px solid #282828; + border-radius: 40px; + width: 68%; + height: 24px; + outline: none; + margin: 0 auto; + display: block; + } +} diff --git a/src/Reducers/Search.js b/src/Reducers/Search.js new file mode 100644 index 0000000..aa4e271 --- /dev/null +++ b/src/Reducers/Search.js @@ -0,0 +1,16 @@ +import * as actions from '../Actions/Search' + +const searchReducer = (state = { albums: [] }, action) => { + switch(action.type) { + case actions.types.SEARCH_RESOLVED: return { + albums: action.data + } + case actions.types.SEARCH_REQUESTED: return { + ...state, + query: action.query + } + default: return state + } +} + +export default searchReducer diff --git a/src/Reducers/index.js b/src/Reducers/index.js index 2d78de3..248018e 100644 --- a/src/Reducers/index.js +++ b/src/Reducers/index.js @@ -1,8 +1,8 @@ import { combineReducers } from 'redux' import { routerReducer } from 'react-router-redux' -import Buddy from './Buddy' +import Search from './Search' export default combineReducers({ - Buddy, + Search, routing: routerReducer -}) \ No newline at end of file +}) diff --git a/src/Sagas/Search.js b/src/Sagas/Search.js new file mode 100644 index 0000000..a015ee8 --- /dev/null +++ b/src/Sagas/Search.js @@ -0,0 +1,17 @@ +import { call, put, takeLatest } from 'redux-saga/effects' +import api from '~/API/searchApi' +import { types } from '~/Actions/Search' + +function* searchRequestHandler({ query }) { + try { + const res = yield call(api.search, query) + yield put({ type: types.SEARCH_RESOLVED, data: res }) + } + catch(e) { + yield put({ type: types.SEARCH_REJECTED, error: e }) + } +} + +export default function* () { + yield takeLatest(types.SEARCH_REQUESTED, searchRequestHandler) +} diff --git a/src/Sagas/index.js b/src/Sagas/index.js index a07638c..f247fd5 100644 --- a/src/Sagas/index.js +++ b/src/Sagas/index.js @@ -1,7 +1,8 @@ import { fork } from 'redux-saga/effects' +import searchSaga from './Search' export default function* () { yield [ - + fork(searchSaga) ] -} \ No newline at end of file +} diff --git a/src/index.js b/src/index.js index e403f9d..d8263e9 100644 --- a/src/index.js +++ b/src/index.js @@ -4,16 +4,16 @@ import store from './store' import ReactDOM from 'react-dom' import { Provider } from 'react-redux' import { Router, Route, browserHistory } from 'react-router' -import { routerReducer, syncHistoryWithStore } from 'react-router-redux' +import { syncHistoryWithStore } from 'react-router-redux' -import Hello from './Components/Hello' +import Main from './Components/Main/Main' const __store = store() ReactDOM.render(( - - - + + + ), document.body.appendChild(document.createElement('div')) -) \ No newline at end of file +) diff --git a/src/index.scss b/src/index.scss index ab16aae..8717042 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1,2 +1,8 @@ // Import from npm @import '~normalize.css'; +@import url('https://fonts.googleapis.com/css?family=Open+Sans'); + +body { + background-color: #181818; + font-family: "Open Sans"; +} diff --git a/src/store.js b/src/store.js index 64f9926..095135d 100644 --- a/src/store.js +++ b/src/store.js @@ -26,4 +26,4 @@ export default () => { sagaMiddleware.run(Saga) return store -} \ No newline at end of file +} diff --git a/webpack.config.js b/webpack.config.js index 7bccb29..784b6ff 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -110,4 +110,4 @@ module.exports = { }, modules: [ path.join(__dirname, 'node_modules') ] } -} \ No newline at end of file +}