Permalink
Browse files

Step 4: Search for a show

  • Loading branch information...
hiquest committed Sep 20, 2017
1 parent 29bed05 commit 2a8dd0b6c97e4273318b977603827dc9cb6b59b3
Showing with 633 additions and 15 deletions.
  1. +4 −1 package.json
  2. +1 −0 src/index.html
  3. +57 −0 src/js/api.js
  4. +68 −3 src/js/components/search.jsx
  5. +56 −0 src/js/components/show.jsx
  6. +78 −0 src/js/store.js
  7. +43 −0 src/main.scss
  8. +26 −0 src/styles/animations.scss
  9. +68 −0 src/styles/basic.scss
  10. +100 −0 src/styles/components.scss
  11. +132 −11 yarn.lock
@@ -13,8 +13,11 @@
"dependencies": {
"moment": "^2.18.1",
"react": "^15.6.1",
"react-addons-css-transition-group": "^15.6.0",
"react-dom": "^15.6.1",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2"
"react-router-dom": "^4.2.2",
"unzip": "^0.1.11",
"xml2js": "^0.4.19"
}
}
@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<title>ElectroTV</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/photon/0.1.2-alpha/css/photon.min.css" />
<link rel="stylesheet" href="main.scss" />
</head>
<body>
<div id='root'></div>
@@ -0,0 +1,57 @@
import req from 'request';
import { parseString } from 'xml2js';
import fs from 'fs';
import unzip from 'unzip';
import http from 'http';
export default { search, download };
const API_HOST = "http://thetvdb.com";
const KEY = process.env.THETVDB_API_KEY;
function search(q, cb) {
const url = `${API_HOST}/api/GetSeries.php?seriesname=${q}`;
get(url, (res) => {
cb((res['Data']['Series'] || []).map(i => {
const out = {
id: i.id[0],
name: i.SeriesName[0],
overview: (i.Overview || [])[0] || 'No Overview'
};
if (i.banner) {
out.banner = `${API_HOST}/banners/${i.banner[0]}`;
}
return out;
}));
});
}
function get(url, cb) {
req(url, (err, resp, body) => {
if (err) throw `Error while req ${url}: ${err}`;
if (resp.statusCode != 200) throw `Error while req ${url}: code — ${resp.statusCode}`;
parseString(body, (err, res) => {
if (err) throw `Error parsing response from ${url}: ${err}`;
cb(res);
});
});
}
function download(id, to, cb) {
const zip_url = `${API_HOST}/api/${KEY}/series/${id}/all/en.zip`;
return dwn(zip_url, `${to}.zip`, () =>
fs.createReadStream(`${to}.zip`)
.pipe(unzip.Extract({path: to}))
.on('close', () => cb())
);
}
function dwn(url, to, cb) {
const file = fs.createWriteStream(to);
http.get(url, (resp) => {
resp.pipe(file);
file.on('finish', () => file.close(cb) );
});
}
@@ -1,5 +1,70 @@
import React from 'react';
import _ from 'lodash';
export default () => (
<div>Searh form is going to be here</div>
);
import api from '../api';
import Show from './show';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
const Search = React.createClass({
getInitialState () {
return {
loading: false,
results: []
};
},
changed({ target: { value } }) {
loadDb(this, value);
},
render() {
return (
<div>
<form>
<div className="form-group">
<input type="email" className="form-control" placeholder="Search for a TV Show"
autoFocus onChange={this.changed}></input>
</div>
</form>
<section className='results'>
{this.state.loading ? Spinner() : renderResults(this, this.state.results) }
</section>
</div>
);
}
});
function renderResults(vm, res) {
return (
<ReactCSSTransitionGroup
transitionName="fade"
transitionAppear={true}
transitionAppearTimeout={300}
transitionEnterTimeout={500}
transitionLeaveTimeout={300}>
{ res.map(i => ( <Show show={i} key={i.id} /> )) }
</ReactCSSTransitionGroup>
);
}
const loadDb = _.debounce((vm, q) => load(vm, q), 500);
function load(vm, q) {
vm.setState({ loading: true, results: [] });
api.search(q, (res = []) => {
vm.setState({ loading: false, results: res });
});
}
function Spinner() {
return (
<div className="spinner">
<div className="double-bounce1"></div>
<div className="double-bounce2"></div>
</div>
);
}
export default Search;
@@ -0,0 +1,56 @@
import React from 'react';
import store from '../store';
export default class extends React.Component {
constructor(props) {
super(props);
this.state = {
show: this.props.show,
loading: false
};
}
componentDidMount() {
const show = this.state.show;
show.followed = store.isFollowed(this.state.show.id);
this.setState(Object.assign(this.state, { show }));
}
render() {
const i = this.state.show;
const ovw = i.overview.length > 128 ? i.overview.substring(0, 128) + "..." : i.overview;
return (
<div className="series card">
<img src={i.banner} />
<h3>{ i.name }</h3>
<p> { ovw } </p>
<div className='action'>
<button
className={ cl(this.state.downloading, this.state.show.followed) }
disabled={this.state.downloading}
onClick={e => follow(this, i)}>
<span className="icon icon-star"></span>
<span className='f1'>FOLLOW</span>
<span className='f2'>FOLLOWING</span>
<span className='f3'>UNFOLLOW</span>
</button>
</div>
</div>
);
}
}
function cl(downloading, followed) {
const out = ['btn', 'btn-large', 'btn-primary'];
downloading && out.push('downloading');
followed && out.push('followed');
return out.join(' ');
}
function follow(vm, i) {
vm.setState({ downloading: true, show: i });
store.add(i.id, () => {
vm.setState({ downloading: false, show: i });
});
}
@@ -0,0 +1,78 @@
import fs from 'fs';
import async from 'async';
import path from 'path';
import xml2js from 'xml2js';
import rimraf from 'rimraf';
import api from './api';
export default {
isFollowed,
readAll,
updateAll,
add
};
const BASE = `${process.env['HOME']}/.eltv`;
const BASE_STORE = `${BASE}/store`;
function add(id, cb) {
if (!fs.existsSync(BASE)) { fs.mkdirSync(BASE); }
if (!fs.existsSync(BASE_STORE)) { fs.mkdirSync(BASE_STORE); }
api.download(id, `${BASE_STORE}/${id}`, () => {
cb();
});
}
function isFollowed(id) {
return fs.existsSync(`${BASE_STORE}/${id}`);
}
function readAll(cb) {
if (!fs.existsSync(BASE_STORE)) {
throw 'BASE_STORE directory doesn not exist';
}
return async.map(available(), readSeries, cb);
}
function updateAll(cb) {
return async.map(available(), updateOne, cb);
}
function updateOne(id, cb) {
console.log(`Started ${id}`);
rm(id, () =>
add(id, () => {
console.log(`Finished ${id}`);
cb();
})
);
}
function rm(id, cb) {
let files = [
`${BASE_STORE}/${id}.zip`,
`${BASE_STORE}/${id}`
];
return async.map(files, rimraf, cb);
}
function available() {
return fs
.readdirSync(BASE_STORE)
.filter(f => fs.statSync(path.join(BASE_STORE, f)).isDirectory());
}
function readSeries(id, cb) {
const xml_file = `${BASE_STORE}/${id}/en.xml`;
if (!fs.existsSync(xml_file)) throw "Could not find the show in the local store";
const str = fs.readFileSync(xml_file);
const parser = new xml2js.Parser();
parser.parseString(str, function(err, result) {
if (err) throw err;
return cb('', result);
});
}
@@ -0,0 +1,43 @@
// Vars
$main: #875468;
@import "./src/styles/basic";
@import "./src/styles/components";
@import "./src/styles/animations";
.results {
overflow: hidden;
clear: both;
margin-top: 20px;
}
.episodes {
overflow: hidden;
clear: both;
margin-top: 22px;
.item {
height: 400px;
.show {
font-size: 16px;
text-transform: uppercase;
color: #aaa;
margin: 0 0 5px 0;
}
}
}
.groupHead {
display: inline-block;
padding: 3px 8px;
background-color: lighten($main, 50%);
border-radius: 6px;
}
// HELPERS
.firm {
color: $main;
}
@@ -0,0 +1,26 @@
.fade-enter {
opacity: 0.01;
}
.fade-enter.fade-enter-active {
opacity: 1;
transition: opacity 500ms ease-in;
}
.fade-leave {
opacity: 1;
}
.fade-leave.fade-leave-active {
opacity: 0.01;
transition: opacity 300ms ease-in;
}
.fade-appear {
opacity: 0.01;
}
.fade-appear.fade-appear-active {
opacity: 1;
transition: opacity .3s ease-in;
}
Oops, something went wrong.

0 comments on commit 2a8dd0b

Please sign in to comment.