1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
env
|
env
|
||||||
|
shows.db
|
||||||
|
|
||||||
yarn.lock
|
yarn.lock
|
||||||
*/yarn.lock
|
*/yarn.lock
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import Notifications, {notify} from 'react-notify-toast';
|
|
||||||
|
|
||||||
// StyleComponents
|
|
||||||
import movieStyle from './styles/movieObjectStyle.jsx';
|
|
||||||
|
|
||||||
var MediaQuery = require('react-responsive');
|
|
||||||
|
|
||||||
import RequestButton from './buttons/request_button.jsx';
|
|
||||||
|
|
||||||
import { fetchJSON } from './http.jsx';
|
|
||||||
|
|
||||||
class MovieObject {
|
|
||||||
constructor(object) {
|
|
||||||
this.id = object.id;
|
|
||||||
this.title = object.title;
|
|
||||||
this.year = object.year;
|
|
||||||
this.type = object.type;
|
|
||||||
// Check if object.poster != undefined
|
|
||||||
this.rating = object.rating;
|
|
||||||
this.poster = object.poster;
|
|
||||||
this.background = object.background;
|
|
||||||
this.matchedInPlex = object.matchedInPlex;
|
|
||||||
this.summary = object.summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
requestExisting(movie) {
|
|
||||||
console.log('Exists', movie);
|
|
||||||
}
|
|
||||||
|
|
||||||
requestMovie() {
|
|
||||||
// fetch('http://localhost:31459/api/v1/plex/request/' + this.id + '?type='+this.type, {
|
|
||||||
// fetch('https://apollo.kevinmidboe.com/api/v1/plex/request/' + this.id + '?type='+this.type, {
|
|
||||||
// method: 'POST'
|
|
||||||
// });
|
|
||||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/request/' + this.id + '?type='+this.type, 'POST')
|
|
||||||
.then((response) => {
|
|
||||||
console.log(response);
|
|
||||||
})
|
|
||||||
|
|
||||||
notify.show(this.title + ' requested!', 'success', 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
getElement(index) {
|
|
||||||
const element_key = index + this.id;
|
|
||||||
|
|
||||||
// TODO set the poster image async by updating the dom after this is returned
|
|
||||||
if (this.poster == null || this.poster == undefined) {
|
|
||||||
var posterPath = 'https://openclipart.org/image/2400px/svg_to_png/211479/Simple-Image-Not-Found-Icon.png'
|
|
||||||
} else {
|
|
||||||
var posterPath = 'https://image.tmdb.org/t/p/w300' + this.poster;
|
|
||||||
}
|
|
||||||
var backgroundPath = 'https://image.tmdb.org/t/p/w640_and_h360_bestv2/' + this.background;
|
|
||||||
|
|
||||||
var foundInPlex;
|
|
||||||
if (this.matchedInPlex) {
|
|
||||||
foundInPlex = <button onClick={() => {this.requestExisting(this)}}
|
|
||||||
style={movieStyle.requestButton}><span>Request Anyway</span></button>;
|
|
||||||
} else {
|
|
||||||
foundInPlex = <button onClick={() => {this.requestMovie()}}
|
|
||||||
style={movieStyle.requestButton}><span>+ Request</span></button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.type === 'movie')
|
|
||||||
var themoviedbLink = 'https://www.themoviedb.org/movie/' + this.id
|
|
||||||
else if (this.type === 'show')
|
|
||||||
var themoviedbLink = 'https://www.themoviedb.org/tv/' + this.id
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// TODO add request button class
|
|
||||||
return (
|
|
||||||
<div key={element_key}>
|
|
||||||
<Notifications />
|
|
||||||
<div style={movieStyle.resultItem} key={this.id}>
|
|
||||||
<MediaQuery minWidth={600}>
|
|
||||||
<div style={movieStyle.resultPoster}>
|
|
||||||
<img style={movieStyle.resultPosterImg} id='poster' src={posterPath}></img>
|
|
||||||
</div>
|
|
||||||
</MediaQuery>
|
|
||||||
<div>
|
|
||||||
<MediaQuery minWidth={600}>
|
|
||||||
<span style={movieStyle.resultTitleLarge}>{this.title}</span>
|
|
||||||
<br></br>
|
|
||||||
<span style={movieStyle.yearRatingLarge}>Released: { this.year } | Rating: {this.rating}</span>
|
|
||||||
<br></br>
|
|
||||||
<span style={movieStyle.summary}>{this.summary}</span>
|
|
||||||
<br></br>
|
|
||||||
</MediaQuery>
|
|
||||||
|
|
||||||
|
|
||||||
<MediaQuery maxWidth={600}>
|
|
||||||
<img src={ backgroundPath } style={movieStyle.background}></img>
|
|
||||||
<span style={movieStyle.resultTitleSmall}>{this.title}</span>
|
|
||||||
<br></br>
|
|
||||||
<span style={movieStyle.yearRatingSmall}>Released: {this.year} | Rating: {this.rating}</span>
|
|
||||||
</MediaQuery>
|
|
||||||
|
|
||||||
|
|
||||||
<span className='imdbLogo'>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div style={movieStyle.buttons}>
|
|
||||||
{foundInPlex}
|
|
||||||
<a href={themoviedbLink}>
|
|
||||||
<button style={movieStyle.tmdbButton}><span>Info</span></button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<MediaQuery maxWidth={600}>
|
|
||||||
<br></br>
|
|
||||||
</MediaQuery>
|
|
||||||
<div style={movieStyle.row}>
|
|
||||||
<div style={movieStyle.itemDivider}></div>
|
|
||||||
</div>
|
|
||||||
</div>)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MovieObject;
|
|
||||||
137
client/app/components/SearchObject.jsx
Normal file
137
client/app/components/SearchObject.jsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Notifications, {notify} from 'react-notify-toast';
|
||||||
|
|
||||||
|
// StyleComponents
|
||||||
|
import searchObjectCSS from './styles/searchObject.jsx';
|
||||||
|
import buttonsCSS from './styles/buttons.jsx';
|
||||||
|
|
||||||
|
var MediaQuery = require('react-responsive');
|
||||||
|
|
||||||
|
import { fetchJSON } from './http.jsx';
|
||||||
|
|
||||||
|
import Interactive from 'react-interactive';
|
||||||
|
|
||||||
|
|
||||||
|
class SearchObject {
|
||||||
|
constructor(object) {
|
||||||
|
this.id = object.id;
|
||||||
|
this.title = object.title;
|
||||||
|
this.year = object.year;
|
||||||
|
this.type = object.type;
|
||||||
|
this.rating = object.rating;
|
||||||
|
this.poster = object.poster;
|
||||||
|
this.background = object.background;
|
||||||
|
this.matchedInPlex = object.matchedInPlex;
|
||||||
|
this.summary = object.summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestExisting(movie) {
|
||||||
|
console.log('Exists', movie);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestMovie() {
|
||||||
|
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/request/' + this.id + '?type='+this.type, 'POST')
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response);
|
||||||
|
notify.show(this.title + ' requested!', 'success', 3000);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('Request movie fetch went wrong: '+ e);
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getElement(index) {
|
||||||
|
const element_key = index + this.id;
|
||||||
|
|
||||||
|
if (this.poster == null || this.poster == undefined) {
|
||||||
|
var posterPath = 'https://openclipart.org/image/2400px/svg_to_png/211479/Simple-Image-Not-Found-Icon.png'
|
||||||
|
} else {
|
||||||
|
var posterPath = 'https://image.tmdb.org/t/p/w300' + this.poster;
|
||||||
|
}
|
||||||
|
var backgroundPath = 'https://image.tmdb.org/t/p/w640_and_h360_bestv2/' + this.background;
|
||||||
|
|
||||||
|
var foundInPlex;
|
||||||
|
if (this.matchedInPlex) {
|
||||||
|
foundInPlex = <Interactive
|
||||||
|
as='button'
|
||||||
|
onClick={() => {this.requestExisting(this)}}
|
||||||
|
style={buttonsCSS.submit}
|
||||||
|
focus={buttonsCSS.submit_hover}
|
||||||
|
hover={buttonsCSS.submit_hover}>
|
||||||
|
|
||||||
|
<span>Request Anyway</span>
|
||||||
|
</Interactive>;
|
||||||
|
} else {
|
||||||
|
foundInPlex = <Interactive
|
||||||
|
as='button'
|
||||||
|
onClick={() => {this.requestMovie()}}
|
||||||
|
style={buttonsCSS.submit}
|
||||||
|
focus={buttonsCSS.submit_hover}
|
||||||
|
hover={buttonsCSS.submit_hover}>
|
||||||
|
|
||||||
|
<span>+ Request</span>
|
||||||
|
</Interactive>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type === 'movie')
|
||||||
|
var themoviedbLink = 'https://www.themoviedb.org/movie/' + this.id
|
||||||
|
else if (this.type === 'show')
|
||||||
|
var themoviedbLink = 'https://www.themoviedb.org/tv/' + this.id
|
||||||
|
|
||||||
|
// TODO go away from using mediaQuery, and create custom resizer
|
||||||
|
return (
|
||||||
|
<div key={element_key}>
|
||||||
|
<Notifications />
|
||||||
|
|
||||||
|
<div style={searchObjectCSS.container} key={this.id}>
|
||||||
|
<MediaQuery minWidth={600}>
|
||||||
|
<div style={searchObjectCSS.posterContainer}>
|
||||||
|
<img style={searchObjectCSS.posterImage} id='poster' src={posterPath}></img>
|
||||||
|
</div>
|
||||||
|
<span style={searchObjectCSS.title_large}>{this.title}</span>
|
||||||
|
<br></br>
|
||||||
|
<span style={searchObjectCSS.stats_large}>Released: { this.year } | Rating: {this.rating}</span>
|
||||||
|
<br></br>
|
||||||
|
<span style={searchObjectCSS.summary}>{this.summary}</span>
|
||||||
|
<br></br>
|
||||||
|
</MediaQuery>
|
||||||
|
|
||||||
|
<MediaQuery maxWidth={600}>
|
||||||
|
<img src={ backgroundPath } style={searchObjectCSS.backgroundImage}></img>
|
||||||
|
<span style={searchObjectCSS.title_small}>{this.title}</span>
|
||||||
|
<br></br>
|
||||||
|
<span style={searchObjectCSS.stats_small}>Released: {this.year} | Rating: {this.rating}</span>
|
||||||
|
</MediaQuery>
|
||||||
|
|
||||||
|
<div style={searchObjectCSS.buttons}>
|
||||||
|
{foundInPlex}
|
||||||
|
|
||||||
|
<a href={themoviedbLink}>
|
||||||
|
<Interactive
|
||||||
|
as='button'
|
||||||
|
hover={buttonsCSS.info_hover}
|
||||||
|
focus={buttonsCSS.info_hover}
|
||||||
|
style={buttonsCSS.info}>
|
||||||
|
|
||||||
|
<span>Info</span>
|
||||||
|
</Interactive>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MediaQuery maxWidth={600}>
|
||||||
|
<br />
|
||||||
|
</MediaQuery>
|
||||||
|
|
||||||
|
<div style={searchObjectCSS.dividerRow}>
|
||||||
|
<div style={searchObjectCSS.itemDivider}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchObject;
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import MovieObject from './MovieObject.jsx';
|
|
||||||
|
|
||||||
// StyleComponents
|
|
||||||
import searchStyle from './styles/searchRequestStyle.jsx';
|
|
||||||
import movieStyle from './styles/movieObjectStyle.jsx';
|
|
||||||
|
|
||||||
import URI from 'urijs';
|
import URI from 'urijs';
|
||||||
import InfiniteScroll from 'react-infinite-scroller';
|
import InfiniteScroll from 'react-infinite-scroller';
|
||||||
|
|
||||||
|
// StyleComponents
|
||||||
|
import searchRequestCSS from './styles/searchRequestStyle.jsx';
|
||||||
|
|
||||||
|
import SearchObject from './SearchObject.jsx';
|
||||||
|
import Loading from './images/loading.jsx'
|
||||||
|
|
||||||
import { fetchJSON } from './http.jsx';
|
import { fetchJSON } from './http.jsx';
|
||||||
import { getCookie } from './Cookie.jsx';
|
import { getCookie } from './Cookie.jsx';
|
||||||
|
|
||||||
@@ -29,7 +29,8 @@ class SearchRequest extends React.Component {
|
|||||||
page: 1,
|
page: 1,
|
||||||
resultHeader: '',
|
resultHeader: '',
|
||||||
loadResults: false,
|
loadResults: false,
|
||||||
scrollHasMore: true
|
scrollHasMore: true,
|
||||||
|
loading: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.allowedListTypes = ['discover', 'popular', 'nowplaying', 'upcoming']
|
this.allowedListTypes = ['discover', 'popular', 'nowplaying', 'upcoming']
|
||||||
@@ -88,9 +89,9 @@ class SearchRequest extends React.Component {
|
|||||||
this.state.page = 1;
|
this.state.page = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeLoading() {
|
setLoading(value) {
|
||||||
this.setState({
|
this.setState({
|
||||||
responseMovieList: 'Loading...'
|
loading: value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,10 +123,7 @@ class SearchRequest extends React.Component {
|
|||||||
|
|
||||||
// Here we first call api for a search with the input uri, handle any errors
|
// Here we first call api for a search with the input uri, handle any errors
|
||||||
// and fill the reponseData from api into the state of reponseMovieList as movieObjects
|
// and fill the reponseData from api into the state of reponseMovieList as movieObjects
|
||||||
callSearchFillMovieList(uri) {
|
callSearchFillMovieList(uri) {
|
||||||
// Write loading animation
|
|
||||||
// this.writeLoading();
|
|
||||||
|
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(() => this.callURI(uri, 'GET'))
|
.then(() => this.callURI(uri, 'GET'))
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -152,7 +150,7 @@ class SearchRequest extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert to json and update the state of responseMovieList with the results of the api call
|
// Convert to json and update the state of responseMovieList with the results of the api call
|
||||||
// mapped as a movieObject.
|
// mapped as a SearchObject.
|
||||||
response.json()
|
response.json()
|
||||||
.then(responseData => {
|
.then(responseData => {
|
||||||
if (this.state.page === 1) {
|
if (this.state.page === 1) {
|
||||||
@@ -180,7 +178,6 @@ class SearchRequest extends React.Component {
|
|||||||
|
|
||||||
callListFillMovieList(uri) {
|
callListFillMovieList(uri) {
|
||||||
// Write loading animation
|
// Write loading animation
|
||||||
// this.writeLoading();
|
|
||||||
|
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(() => this.callURI(uri, 'GET', undefined))
|
.then(() => this.callURI(uri, 'GET', undefined))
|
||||||
@@ -198,7 +195,7 @@ class SearchRequest extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert to json and update the state of responseMovieList with the results of the api call
|
// Convert to json and update the state of responseMovieList with the results of the api call
|
||||||
// mapped as a movieObject.
|
// mapped as a SearchObject.
|
||||||
response.json()
|
response.json()
|
||||||
.then(responseData => {
|
.then(responseData => {
|
||||||
if (this.state.page === 1) {
|
if (this.state.page === 1) {
|
||||||
@@ -218,6 +215,7 @@ class SearchRequest extends React.Component {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log('Something went wrong when fetching query.', error)
|
console.log('Something went wrong when fetching query.', error)
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,10 +285,10 @@ class SearchRequest extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When called passes the variable to MovieObject and calls it's interal function for
|
// When called passes the variable to SearchObject and calls it's interal function for
|
||||||
// generating the wanted HTML
|
// generating the wanted HTML
|
||||||
createMovieObjects(item, index) {
|
createMovieObjects(item, index) {
|
||||||
let movie = new MovieObject(item);
|
let movie = new SearchObject(item);
|
||||||
return movie.getElement(index);
|
return movie.getElement(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,12 +343,12 @@ class SearchRequest extends React.Component {
|
|||||||
|
|
||||||
movieToggle() {
|
movieToggle() {
|
||||||
if (this.state.movieFilter)
|
if (this.state.movieFilter)
|
||||||
return <span style={searchStyle.searchFilterActive}
|
return <span style={searchRequestCSS.searchFilterActive}
|
||||||
className="search_category hvrUnderlineFromCenter"
|
className="search_category hvrUnderlineFromCenter"
|
||||||
onClick={() => {this.toggleFilter('movies')}}
|
onClick={() => {this.toggleFilter('movies')}}
|
||||||
id="category_active">Movies</span>
|
id="category_active">Movies</span>
|
||||||
else
|
else
|
||||||
return <span style={searchStyle.searchFilterNotActive}
|
return <span style={searchRequestCSS.searchFilterNotActive}
|
||||||
className="search_category hvrUnderlineFromCenter"
|
className="search_category hvrUnderlineFromCenter"
|
||||||
onClick={() => {this.toggleFilter('movies')}}
|
onClick={() => {this.toggleFilter('movies')}}
|
||||||
id="category_active">Movies</span>
|
id="category_active">Movies</span>
|
||||||
@@ -358,12 +356,12 @@ class SearchRequest extends React.Component {
|
|||||||
|
|
||||||
showToggle() {
|
showToggle() {
|
||||||
if (this.state.showFilter)
|
if (this.state.showFilter)
|
||||||
return <span style={searchStyle.searchFilterActive}
|
return <span style={searchRequestCSS.searchFilterActive}
|
||||||
className="search_category hvrUnderlineFromCenter"
|
className="search_category hvrUnderlineFromCenter"
|
||||||
onClick={() => {this.toggleFilter('shows')}}
|
onClick={() => {this.toggleFilter('shows')}}
|
||||||
id="category_active">TV Shows</span>
|
id="category_active">TV Shows</span>
|
||||||
else
|
else
|
||||||
return <span style={searchStyle.searchFilterNotActive}
|
return <span style={searchRequestCSS.searchFilterNotActive}
|
||||||
className="search_category hvrUnderlineFromCenter"
|
className="search_category hvrUnderlineFromCenter"
|
||||||
onClick={() => {this.toggleFilter('shows')}}
|
onClick={() => {this.toggleFilter('shows')}}
|
||||||
id="category_active">TV Shows</span>
|
id="category_active">TV Shows</span>
|
||||||
@@ -379,21 +377,21 @@ class SearchRequest extends React.Component {
|
|||||||
pageStart={0}
|
pageStart={0}
|
||||||
loadMore={this.pageForwards.bind(this)}
|
loadMore={this.pageForwards.bind(this)}
|
||||||
hasMore={this.state.scrollHasMore}
|
hasMore={this.state.scrollHasMore}
|
||||||
loader={loader}
|
loader={<Loading />}
|
||||||
initialLoad={this.state.loadResults}>
|
initialLoad={this.state.loadResults}>
|
||||||
|
|
||||||
<MediaQuery minWidth={600}>
|
<MediaQuery minWidth={600}>
|
||||||
<div style={searchStyle.body}>
|
<div style={searchRequestCSS.body}>
|
||||||
<div className='backgroundHeader' style={searchStyle.backgroundLargeHeader}>
|
<div className='backgroundHeader' style={searchRequestCSS.backgroundLargeHeader}>
|
||||||
<div className='pageTitle' style={searchStyle.pageTitle}>
|
<div className='pageTitle' style={searchRequestCSS.pageTitle}>
|
||||||
<span style={searchStyle.pageTitleLargeSpan}>Request new content</span>
|
<span style={searchRequestCSS.pageTitleLargeSpan}>Request new content</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='box' style={searchStyle.box}>
|
<div className='box' style={searchRequestCSS.box}>
|
||||||
<div style={searchStyle.searchLargeContainer}>
|
<div style={searchRequestCSS.searchLargeContainer}>
|
||||||
<span style={searchStyle.searchIcon}><i className="fa fa-search"></i></span>
|
<span style={searchRequestCSS.searchIcon}><i className="fa fa-search"></i></span>
|
||||||
|
|
||||||
<input style={searchStyle.searchLargeBar} type="text" id="search" placeholder="Search for new content..."
|
<input style={searchRequestCSS.searchLargeBar} type="text" id="search" placeholder="Search for new content..."
|
||||||
onKeyPress={(event) => this._handleQueryKeyPress(event)}
|
onKeyPress={(event) => this._handleQueryKeyPress(event)}
|
||||||
onChange={event => this.updateQueryState(event)}
|
onChange={event => this.updateQueryState(event)}
|
||||||
value={this.state.searchQuery}/>
|
value={this.state.searchQuery}/>
|
||||||
@@ -402,27 +400,27 @@ class SearchRequest extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id='requestMovieList' ref='requestMovieList' style={searchStyle.requestWrapper}>
|
<div id='requestMovieList' ref='requestMovieList' style={searchRequestCSS.requestWrapper}>
|
||||||
<span style={searchStyle.resultLargeHeader}>{this.state.resultHeader}</span>
|
<span style={searchRequestCSS.resultLargeHeader}>{this.state.resultHeader}</span>
|
||||||
<br></br><br></br>
|
<br></br><br></br>
|
||||||
|
|
||||||
{this.state.responseMovieList}
|
{this.state.responseMovieList}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MediaQuery>
|
</MediaQuery>
|
||||||
|
|
||||||
<MediaQuery maxWidth={600}>
|
<MediaQuery maxWidth={600}>
|
||||||
<div style={searchStyle.body}>
|
<div style={searchRequestCSS.body}>
|
||||||
<div className='backgroundHeader' style={searchStyle.backgroundSmallHeader}>
|
<div className='backgroundHeader' style={searchRequestCSS.backgroundSmallHeader}>
|
||||||
<div className='pageTitle' style={searchStyle.pageTitle}>
|
<div className='pageTitle' style={searchRequestCSS.pageTitle}>
|
||||||
<span style={searchStyle.pageTitleSmallSpan}>Request new content</span>
|
<span style={searchRequestCSS.pageTitleSmallSpan}>Request new content</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='box' style={searchStyle.box}>
|
<div className='box' style={searchRequestCSS.box}>
|
||||||
<div style={searchStyle.searchSmallContainer}>
|
<div style={searchRequestCSS.searchSmallContainer}>
|
||||||
<span style={searchStyle.searchIcon}><i className="fa fa-search"></i></span>
|
<span style={searchRequestCSS.searchIcon}><i className="fa fa-search"></i></span>
|
||||||
|
|
||||||
<input style={searchStyle.searchSmallBar} type="text" id="search" placeholder="Search for new content..."
|
<input style={searchRequestCSS.searchSmallBar} type="text" id="search" placeholder="Search for new content..."
|
||||||
onKeyPress={(event) => this._handleQueryKeyPress(event)}
|
onKeyPress={(event) => this._handleQueryKeyPress(event)}
|
||||||
onChange={event => this.updateQueryState(event)}
|
onChange={event => this.updateQueryState(event)}
|
||||||
value={this.state.searchQuery}/>
|
value={this.state.searchQuery}/>
|
||||||
@@ -431,11 +429,11 @@ class SearchRequest extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id='requestMovieList' ref='requestMovieList' style={searchStyle.requestWrapper}>
|
<div id='requestMovieList' ref='requestMovieList' style={searchRequestCSS.requestWrapper}>
|
||||||
<span style={searchStyle.resultSmallHeader}>{this.state.resultHeader}</span>
|
<span style={searchRequestCSS.resultSmallHeader}>{this.state.resultHeader}</span>
|
||||||
<br></br><br></br>
|
<br></br><br></br>
|
||||||
|
|
||||||
{this.state.responseMovieList}
|
{this.state.responseMovieList}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MediaQuery>
|
</MediaQuery>
|
||||||
@@ -446,4 +444,4 @@ class SearchRequest extends React.Component {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SearchRequest;
|
export default SearchRequest;
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
/*
|
|
||||||
./app/components/App.jsx
|
|
||||||
|
|
||||||
<FetchData url={"https://apollo.kevinmidboe.com/api/v1/plex/playing"} />
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { HashRouter as Router, Route, Switch, IndexRoute } from 'react-router-dom';
|
|
||||||
|
|
||||||
import LoginForm from './LoginForm/LoginForm.jsx';
|
import LoginForm from './LoginForm/LoginForm.jsx';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
@@ -16,6 +11,8 @@ import { fetchJSON } from '../http.jsx';
|
|||||||
import Sidebar from './Sidebar.jsx';
|
import Sidebar from './Sidebar.jsx';
|
||||||
import AdminRequestInfo from './AdminRequestInfo.jsx';
|
import AdminRequestInfo from './AdminRequestInfo.jsx';
|
||||||
|
|
||||||
|
import adminCSS from '../styles/adminComponent.jsx'
|
||||||
|
|
||||||
|
|
||||||
class AdminComponent extends React.Component {
|
class AdminComponent extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -25,6 +22,7 @@ class AdminComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetches all requested elements and updates the state with response
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/requests/all', 'GET')
|
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/requests/all', 'GET')
|
||||||
.then(result => {
|
.then(result => {
|
||||||
@@ -34,16 +32,9 @@ class AdminComponent extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Displays loginform if not logged in and passes response from
|
||||||
|
// api call to sidebar and infoPanel through props
|
||||||
verifyLoggedIn() {
|
verifyLoggedIn() {
|
||||||
let adminComponentStyle = {
|
|
||||||
sidebar: {
|
|
||||||
float: 'left',
|
|
||||||
},
|
|
||||||
selectedObjectPanel: {
|
|
||||||
float: 'left',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const logged_in = getCookie('logged_in');
|
const logged_in = getCookie('logged_in');
|
||||||
if (!logged_in) {
|
if (!logged_in) {
|
||||||
return <LoginForm />
|
return <LoginForm />
|
||||||
@@ -53,20 +44,21 @@ class AdminComponent extends React.Component {
|
|||||||
let listItemSelected = undefined;
|
let listItemSelected = undefined;
|
||||||
|
|
||||||
const requestParam = this.props.match.params.request;
|
const requestParam = this.props.match.params.request;
|
||||||
|
|
||||||
if (requestParam && this.state.requested_objects !== '') {
|
if (requestParam && this.state.requested_objects !== '') {
|
||||||
selectedRequest = this.state.requested_objects[requestParam]
|
selectedRequest = this.state.requested_objects[requestParam]
|
||||||
listItemSelected = requestParam
|
listItemSelected = requestParam;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={adminComponentStyle.sidebar}>
|
<div style={adminCSS.sidebar}>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
requested_objects={this.state.requested_objects}
|
requested_objects={this.state.requested_objects}
|
||||||
listItemSelected={listItemSelected}
|
listItemSelected={listItemSelected}
|
||||||
style={adminComponentStyle.sidebar} />
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={adminComponentStyle.selectedObjectPanel}>
|
<div style={adminCSS.selectedObjectPanel}>
|
||||||
<AdminRequestInfo
|
<AdminRequestInfo
|
||||||
selectedRequest={selectedRequest}
|
selectedRequest={selectedRequest}
|
||||||
listItemSelected={listItemSelected}
|
listItemSelected={listItemSelected}
|
||||||
|
|||||||
@@ -1,10 +1,31 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { fetchJSON } from '../http.jsx';
|
||||||
|
|
||||||
import PirateSearch from './PirateSearch.jsx'
|
import PirateSearch from './PirateSearch.jsx'
|
||||||
|
|
||||||
|
// Stylesheets
|
||||||
|
import requestInfoCSS from '../styles/adminRequestInfo.jsx'
|
||||||
|
import buttonsCSS from '../styles/buttons.jsx';
|
||||||
|
|
||||||
|
// Interactive button
|
||||||
|
import Interactive from 'react-interactive';
|
||||||
|
|
||||||
class AdminRequestInfo extends Component {
|
class AdminRequestInfo extends Component {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
statusValue: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestInfo = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(props) {
|
||||||
|
this.requestInfo = props.selectedRequest;
|
||||||
|
this.state.statusValue = this.requestInfo.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
userAgent(agent) {
|
userAgent(agent) {
|
||||||
@@ -19,6 +40,35 @@ class AdminRequestInfo extends Component {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateStatusDropdown() {
|
||||||
|
return (
|
||||||
|
<select onChange={ event => this.updateRequestStatus(event) } value={this.state.statusValue}>
|
||||||
|
<option value='requested'>Requested</option>
|
||||||
|
<option value='downloading'>Downloading</option>
|
||||||
|
<option value='downloaded'>Downloaded</option>
|
||||||
|
</select>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRequestStatus(event) {
|
||||||
|
const eventValue = event.target.value;
|
||||||
|
const itemID = this.requestInfo.id;
|
||||||
|
|
||||||
|
const apiData = {
|
||||||
|
type: this.requestInfo.type,
|
||||||
|
status: eventValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/request/' + itemID, 'PUT', apiData)
|
||||||
|
.then((response) => {
|
||||||
|
console.log('Response, updateRequestStatus: ', response)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
statusValue: eventValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
requested_by_user(request_user) {
|
requested_by_user(request_user) {
|
||||||
if (request_user === 'NULL')
|
if (request_user === 'NULL')
|
||||||
return undefined
|
return undefined
|
||||||
@@ -28,57 +78,52 @@ class AdminRequestInfo extends Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
displayInfo() {
|
displayInfo() {
|
||||||
let adminIndexStyle = {
|
const request = this.props.selectedRequest;
|
||||||
wrapper: {
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
headerWrapper: {
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
poster: {
|
|
||||||
float: 'left',
|
|
||||||
minHeight: '450px',
|
|
||||||
},
|
|
||||||
info: {
|
|
||||||
float: 'left',
|
|
||||||
minHeight: '450px',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const request = this.props.selectedRequest;
|
|
||||||
|
|
||||||
if (request) {
|
if (request) {
|
||||||
return (
|
return (
|
||||||
<div style={adminIndexStyle.wrapper}>
|
<div style={requestInfoCSS.wrapper}>
|
||||||
<div style={adminIndexStyle.headerWrapper}>
|
<div style={requestInfoCSS.headerWrapper}>
|
||||||
<span>{request.name} </span>
|
<span>{request.name} </span>
|
||||||
<span>{request.year}</span>
|
<span>{request.year}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={adminIndexStyle.poster}>
|
|
||||||
<img src={'https://image.tmdb.org/t/p/w300/' + request.image_path} />
|
|
||||||
</div>
|
|
||||||
<div style={adminIndexStyle.info}>
|
|
||||||
<span>type: {request.type}</span><br />
|
|
||||||
<span>status: {request.status}</span><br />
|
|
||||||
<span>ip: {request.ip}</span><br />
|
|
||||||
<span>user_agent: {this.userAgent(request.user_agent)}</span><br />
|
|
||||||
<span>request_date: {request.requested_date}</span><br />
|
|
||||||
{ this.requested_by_user(request.requested_by) }
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<PirateSearch
|
|
||||||
name={request.name} />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
<div style={requestInfoCSS.info}>
|
||||||
return (
|
<span>type: {request.type}</span><br />
|
||||||
<div>{this.displayInfo()}</div>
|
|
||||||
);
|
{this.generateStatusDropdown()}<br />
|
||||||
}
|
|
||||||
|
<span>status: {request.status}</span><br />
|
||||||
|
<span>ip: {request.ip}</span><br />
|
||||||
|
<span>user_agent: {this.userAgent(request.user_agent)}</span><br />
|
||||||
|
<span>request_date: {request.requested_date}</span><br />
|
||||||
|
{ this.requested_by_user(request.requested_by) }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Interactive
|
||||||
|
as='button'
|
||||||
|
onClick={() => {}}
|
||||||
|
style={buttonsCSS.edit}
|
||||||
|
focus={buttonsCSS.edit_hover}
|
||||||
|
hover={buttonsCSS.edit_hover}>
|
||||||
|
|
||||||
|
<span>Show info</span>
|
||||||
|
</Interactive>
|
||||||
|
|
||||||
|
<PirateSearch name={request.name} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>{this.displayInfo()}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdminRequestInfo;
|
export default AdminRequestInfo;
|
||||||
@@ -1,18 +1,25 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { fetchJSON } from '../http.jsx';
|
import { fetchJSON } from '../http.jsx';
|
||||||
|
|
||||||
|
// Stylesheets
|
||||||
|
import btnStylesheet from '../styles/buttons.jsx';
|
||||||
|
|
||||||
|
// Interactive button
|
||||||
|
import Interactive from 'react-interactive';
|
||||||
|
|
||||||
|
import Loading from '../images/loading.jsx'
|
||||||
|
|
||||||
class PirateSearch extends Component {
|
class PirateSearch extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.state = {
|
this.state = {
|
||||||
response: [],
|
response: [],
|
||||||
name: '',
|
name: '',
|
||||||
|
loading: '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToDownload(torrent) {
|
sendToDownload(torrent) {
|
||||||
console.log(torrent.magnet)
|
|
||||||
|
|
||||||
let data = {magnet: torrent.magnet}
|
let data = {magnet: torrent.magnet}
|
||||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/add', 'POST', data)
|
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/add', 'POST', data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -24,10 +31,15 @@ class PirateSearch extends Component {
|
|||||||
const query = this.props.name;
|
const query = this.props.name;
|
||||||
const type = this.props.type;
|
const type = this.props.type;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loading: <Loading />
|
||||||
|
})
|
||||||
|
|
||||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
|
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
console.log(response.torrents)
|
console.log(response.torrents)
|
||||||
this.setState({
|
this.setState({
|
||||||
|
loading: '',
|
||||||
response: response.torrents.map((torrent, index) => {
|
response: response.torrents.map((torrent, index) => {
|
||||||
return (
|
return (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
@@ -46,8 +58,20 @@ class PirateSearch extends Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<span>{this.props.name}</span>
|
<div>
|
||||||
<button onClick={() => {this.searchTheBay(this)}}>Load shit</button>
|
<Interactive
|
||||||
|
as='button'
|
||||||
|
onClick={() => {this.searchTheBay()}}
|
||||||
|
style={btnStylesheet.submit}
|
||||||
|
focus={btnStylesheet.submit_hover}
|
||||||
|
hover={btnStylesheet.submit_hover}>
|
||||||
|
|
||||||
|
<span style={{whiteSpace: 'nowrap'}}>Search for torrents</span>
|
||||||
|
</Interactive>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ this.state.loading }
|
||||||
|
|
||||||
<span>{this.state.response}</span>
|
<span>{this.state.response}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,57 +1,213 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Interactive from 'react-interactive';
|
||||||
|
|
||||||
|
import sidebarCSS from '../styles/adminSidebar.jsx'
|
||||||
|
|
||||||
class SidebarComponent extends Component {
|
class SidebarComponent extends Component {
|
||||||
|
|
||||||
generateListElements(index, item) {
|
constructor(props){
|
||||||
if (index == this.props.listItemSelected)
|
super(props)
|
||||||
return (
|
// Constructor with states holding the search query and the element of reponse.
|
||||||
<td>{item.name}</td>
|
this.state = {
|
||||||
)
|
filterValue: '',
|
||||||
else
|
filterQuery: '',
|
||||||
return (
|
requestItemsToBeDisplayed: [],
|
||||||
<td><Link to={{ pathname: '/admin/'+String(index)}}>{item.name}</Link></td>
|
listItemSelected: '',
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
displayRequestedElementsInfo() {
|
|
||||||
if (this.props.requested_objects) {
|
|
||||||
let requestedElement = this.props.requested_objects.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<tr key={index}>
|
|
||||||
{ this.generateListElements(index, item) }
|
|
||||||
<td>{item.status}</td>
|
|
||||||
<td>{item.requested_date}</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table key='requestedTable'>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th><b>Name</b></th>
|
|
||||||
<th><b>Status</b></th>
|
|
||||||
<th><b>Date</b></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{requestedElement}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
// Where we wait for api response to be delivered from parent through props
|
||||||
console.log('sidebar: ', this.props.requested_objects)
|
componentWillReceiveProps(props) {
|
||||||
return (
|
this.state.listItemSelected = props.listItemSelected;
|
||||||
<div>
|
this.displayRequestedElementsInfo(props.requested_objects);
|
||||||
<h1>Hello from the sidebar: </h1>
|
}
|
||||||
<span>{ this.displayRequestedElementsInfo() }</span>
|
|
||||||
</div>
|
// Inputs a date and returns a text string that matches how long it was since
|
||||||
);
|
convertDateToDaysSince(date) {
|
||||||
}
|
var oneDay = 24*60*60*1000;
|
||||||
|
var firstDate = new Date(date);
|
||||||
|
var secondDate = new Date();
|
||||||
|
|
||||||
|
var diffDays = Math.round(Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay));
|
||||||
|
|
||||||
|
switch (diffDays) {
|
||||||
|
case 0:
|
||||||
|
return 'Today';
|
||||||
|
case 1:
|
||||||
|
return '1 day ago'
|
||||||
|
default:
|
||||||
|
return diffDays + ' days ago'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from our dropdown, receives a filter string and checks it with status field
|
||||||
|
// of our request objects.
|
||||||
|
filterItems(filterValue) {
|
||||||
|
let filteredRequestElements = this.props.requested_objects.map((item, index) => {
|
||||||
|
if (item.status === filterValue || filterValue === 'all')
|
||||||
|
return this.generateListElements(index, item);
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
requestItemsToBeDisplayed: filteredRequestElements,
|
||||||
|
filterValue: filterValue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Updates the internal state of the query filter and updates the list to only
|
||||||
|
// display names matching the query. This is real-time filtering.
|
||||||
|
updateFilterQuery(event) {
|
||||||
|
const query = event.target.value;
|
||||||
|
|
||||||
|
let filteredByQuery = this.props.requested_objects.map((item, index) => {
|
||||||
|
if (item.name.toLowerCase().indexOf(query.toLowerCase()) != -1)
|
||||||
|
return this.generateListElements(index, item);
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
requestItemsToBeDisplayed: filteredByQuery,
|
||||||
|
filterQuery: query,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
generateFilterDropdown() {
|
||||||
|
return (
|
||||||
|
<select onChange={ event => this.filterItems(event.target.value) } value={this.state.filterValue}>
|
||||||
|
<option value='all'>All</option>
|
||||||
|
<option value='requested'>Requested</option>
|
||||||
|
<option value='downloading'>Downloading</option>
|
||||||
|
<option value='downloaded'>Downloaded</option>
|
||||||
|
</select>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
generateFilterSearchbar() {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search"
|
||||||
|
placeholder="Filter by name..."
|
||||||
|
onChange={event => this.updateFilterQuery(event)}
|
||||||
|
value={this.state.filterQuery}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A colored bar indicating the status of a item by color.
|
||||||
|
generateRequestIndicator(status) {
|
||||||
|
let statusColor;
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'requested':
|
||||||
|
// Yellow
|
||||||
|
statusColor = '#ffe14d';
|
||||||
|
break;
|
||||||
|
case 'downloading':
|
||||||
|
// Blue
|
||||||
|
statusColor = '#3fc3f3';
|
||||||
|
break;
|
||||||
|
case 'downloaded':
|
||||||
|
// Green
|
||||||
|
statusColor = '#6be682';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
statusColor = 'grey';
|
||||||
|
}
|
||||||
|
|
||||||
|
const indicatorCSS = {
|
||||||
|
width: '100%',
|
||||||
|
height: '4px',
|
||||||
|
marginTop: '3px',
|
||||||
|
backgroundColor: statusColor,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={indicatorCSS}></div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
generateListElements(index, item) {
|
||||||
|
if (index == this.state.listItemSelected) {
|
||||||
|
return (
|
||||||
|
<div style={sidebarCSS.parentElement_selected}>
|
||||||
|
<div style={sidebarCSS.contentContainer}>
|
||||||
|
<span style={sidebarCSS.title}> {item.name } </span>
|
||||||
|
<div style={sidebarCSS.rightContainer}>
|
||||||
|
<span>{ this.convertDateToDaysSince(item.requested_date) }</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>Status: { item.status }</span>
|
||||||
|
<br/>
|
||||||
|
<span>Matches found: 0</span>
|
||||||
|
{ this.generateRequestIndicator(item.status) }
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<Link style={sidebarCSS.link} to={{ pathname: '/admin/'+String(index)}}>
|
||||||
|
<Interactive
|
||||||
|
key={index}
|
||||||
|
style={sidebarCSS.parentElement}
|
||||||
|
as='div'
|
||||||
|
hover={sidebarCSS.parentElement_hover}
|
||||||
|
focus={sidebarCSS.parentElement_hover}
|
||||||
|
active={sidebarCSS.parentElement_active}>
|
||||||
|
|
||||||
|
<span style={sidebarCSS.title}> {item.name } </span>
|
||||||
|
<div style={sidebarCSS.rightContainer}>
|
||||||
|
<span>{ this.convertDateToDaysSince(item.requested_date) }</span>
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
{ this.generateRequestIndicator(item.status) }
|
||||||
|
</Interactive>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is our main loader that gets called when we receive api response through props from parent
|
||||||
|
displayRequestedElementsInfo(requested_objects) {
|
||||||
|
let requestedElement = requested_objects.map((item, index) => {
|
||||||
|
if (['requested', 'downloading', 'downloaded'].indexOf(this.state.filterValue) != -1) {
|
||||||
|
if (item.status === this.state.filterValue){
|
||||||
|
return this.generateListElements(index, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (this.state.filterQuery !== '') {
|
||||||
|
if (item.name.toLowerCase().indexOf(this.state.filterQuery.toLowerCase()) != -1)
|
||||||
|
return this.generateListElements(index, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
return this.generateListElements(index, item);
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
requestItemsToBeDisplayed: requestedElement,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let bodyCSS = sidebarCSS.body;
|
||||||
|
if (typeof InstallTrigger !== 'undefined')
|
||||||
|
bodyCSS.width = '-moz-min-content';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Hello from the sidebar: </h1>
|
||||||
|
{ this.generateFilterDropdown() }
|
||||||
|
{ this.generateFilterSearchbar() }
|
||||||
|
<div key='requestedTable' style={bodyCSS}>
|
||||||
|
{ this.state.requestItemsToBeDisplayed }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SidebarComponent;
|
export default SidebarComponent;
|
||||||
34
client/app/components/images/loading.jsx
Normal file
34
client/app/components/images/loading.jsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function Loading() {
|
||||||
|
return (
|
||||||
|
<div style={{textAlign: 'center'}}>
|
||||||
|
<svg version="1.1"
|
||||||
|
style={{height: '75px'}}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 80 80">
|
||||||
|
<path
|
||||||
|
fill="#e9a131"
|
||||||
|
d="M40,72C22.4,72,8,57.6,8,40C8,22.4,
|
||||||
|
22.4,8,40,8c17.6,0,32,14.4,32,32c0,1.1-0.9,2-2,2
|
||||||
|
s-2-0.9-2-2c0-15.4-12.6-28-28-28S12,24.6,12,40s12.6,
|
||||||
|
28,28,28c1.1,0,2,0.9,2,2S41.1,72,40,72z">
|
||||||
|
|
||||||
|
<animateTransform
|
||||||
|
attributeType="xml"
|
||||||
|
attributeName="transform"
|
||||||
|
type="rotate"
|
||||||
|
from="0 40 40"
|
||||||
|
to="360 40 40"
|
||||||
|
dur="1.0s"
|
||||||
|
repeatCount="indefinite"/>
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Loading;
|
||||||
8
client/app/components/styles/adminComponent.jsx
Normal file
8
client/app/components/styles/adminComponent.jsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
sidebar: {
|
||||||
|
float: 'left',
|
||||||
|
},
|
||||||
|
selectedObjectPanel: {
|
||||||
|
float: 'left',
|
||||||
|
}
|
||||||
|
}
|
||||||
11
client/app/components/styles/adminRequestInfo.jsx
Normal file
11
client/app/components/styles/adminRequestInfo.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export default {
|
||||||
|
wrapper: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
headerWrapper: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
poster: {
|
||||||
|
minHeight: '450px',
|
||||||
|
},
|
||||||
|
}
|
||||||
50
client/app/components/styles/adminSidebar.jsx
Normal file
50
client/app/components/styles/adminSidebar.jsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
export default {
|
||||||
|
body: {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
width: 'min-content',
|
||||||
|
},
|
||||||
|
|
||||||
|
parentElement: {
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '300px',
|
||||||
|
border: '1px solid black',
|
||||||
|
borderRadius: '2px',
|
||||||
|
padding: '4px',
|
||||||
|
margin: '4px',
|
||||||
|
marginLeft: '4px',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
},
|
||||||
|
|
||||||
|
parentElement_hover: {
|
||||||
|
marginLeft: '10px',
|
||||||
|
},
|
||||||
|
|
||||||
|
parentElement_active: {
|
||||||
|
textDecoration: 'none',
|
||||||
|
},
|
||||||
|
|
||||||
|
parentElement_selected: {
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '300px',
|
||||||
|
border: '1px solid black',
|
||||||
|
borderRadius: '2px',
|
||||||
|
padding: '4px',
|
||||||
|
margin: '4px 0px 4px 4px',
|
||||||
|
marginLeft: '10px',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
},
|
||||||
|
|
||||||
|
title: {
|
||||||
|
maxWidth: '70%',
|
||||||
|
display: 'inline-flex',
|
||||||
|
},
|
||||||
|
|
||||||
|
link: {
|
||||||
|
color: 'black',
|
||||||
|
textDecoration: 'none',
|
||||||
|
},
|
||||||
|
|
||||||
|
rightContainer: {
|
||||||
|
float: 'right',
|
||||||
|
},
|
||||||
|
}
|
||||||
80
client/app/components/styles/buttons.jsx
Normal file
80
client/app/components/styles/buttons.jsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
submit: {
|
||||||
|
color: '#e9a131',
|
||||||
|
marginRight: '10px',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '#e9a131 2px solid',
|
||||||
|
borderColor: '#e9a131',
|
||||||
|
borderRadius: '4px',
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '10px',
|
||||||
|
minWidth: '100px',
|
||||||
|
float: 'left',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '800',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
|
||||||
|
submit_hover: {
|
||||||
|
backgroundColor: '#e9a131',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
|
||||||
|
info: {
|
||||||
|
color: '#00d17c',
|
||||||
|
marginRight: '10px',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '#00d17c 2px solid',
|
||||||
|
borderRadius: '4px',
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '10px',
|
||||||
|
minWidth: '100px',
|
||||||
|
float: 'left',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '800',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
|
||||||
|
info_hover: {
|
||||||
|
backgroundColor: '#00d17c',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: {
|
||||||
|
color: '#4a95da',
|
||||||
|
marginRight: '10px',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '#4a95da 2px solid',
|
||||||
|
borderRadius: '4px',
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '10px',
|
||||||
|
minWidth: '100px',
|
||||||
|
float: 'left',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '800',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
|
||||||
|
edit_small: {
|
||||||
|
color: '#4a95da',
|
||||||
|
marginRight: '10px',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '#4a95da 2px solid',
|
||||||
|
borderRadius: '4px',
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '4px',
|
||||||
|
minWidth: '50px',
|
||||||
|
float: 'left',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '800',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
|
||||||
|
edit_hover: {
|
||||||
|
backgroundColor: '#4a95da',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
|
|
||||||
export default {
|
|
||||||
resultItem: {
|
|
||||||
maxWidth: '95%',
|
|
||||||
margin: '0 auto',
|
|
||||||
minHeight: '230px'
|
|
||||||
},
|
|
||||||
|
|
||||||
movie_content: {
|
|
||||||
marginLeft: '15px'
|
|
||||||
},
|
|
||||||
|
|
||||||
resultTitleLarge: {
|
|
||||||
color: 'black',
|
|
||||||
fontSize: '2em',
|
|
||||||
},
|
|
||||||
|
|
||||||
resultTitleSmall: {
|
|
||||||
color: 'black',
|
|
||||||
fontSize: '22px',
|
|
||||||
},
|
|
||||||
|
|
||||||
yearRatingLarge: {
|
|
||||||
fontSize: '0.8em'
|
|
||||||
},
|
|
||||||
|
|
||||||
resultPoster: {
|
|
||||||
float: 'left',
|
|
||||||
zIndex: '3',
|
|
||||||
position: 'relative',
|
|
||||||
marginRight: '30px'
|
|
||||||
},
|
|
||||||
|
|
||||||
background: {
|
|
||||||
width: '100%'
|
|
||||||
},
|
|
||||||
|
|
||||||
yearRatingSmall: {
|
|
||||||
marginTop: '5px',
|
|
||||||
fontSize: '0.8em'
|
|
||||||
},
|
|
||||||
|
|
||||||
resultPosterImg: {
|
|
||||||
border: '2px none',
|
|
||||||
borderRadius: '2px',
|
|
||||||
width: '150px'
|
|
||||||
},
|
|
||||||
|
|
||||||
cornerRibbon: {
|
|
||||||
position: 'absolute',
|
|
||||||
width: '450px',
|
|
||||||
},
|
|
||||||
|
|
||||||
summary: {
|
|
||||||
fontSize: '15px',
|
|
||||||
},
|
|
||||||
|
|
||||||
buttons: {
|
|
||||||
paddingTop: '20px'
|
|
||||||
},
|
|
||||||
|
|
||||||
requestButton: {
|
|
||||||
color: '#e9a131',
|
|
||||||
marginRight: '10px',
|
|
||||||
background: 'white',
|
|
||||||
border: '#e9a131 2px solid',
|
|
||||||
borderRadius: '4px',
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '10px',
|
|
||||||
minWidth: '100px',
|
|
||||||
float: 'left',
|
|
||||||
fontSize: '13px',
|
|
||||||
fontWeight: '800',
|
|
||||||
cursor: 'pointer'
|
|
||||||
},
|
|
||||||
|
|
||||||
tmdbButton: {
|
|
||||||
color: '#00d17c',
|
|
||||||
marginRight: '10px',
|
|
||||||
background: 'white',
|
|
||||||
border: '#00d17c 2px solid',
|
|
||||||
borderRadius: '4px',
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '10px',
|
|
||||||
minWidth: '100px',
|
|
||||||
float: 'left',
|
|
||||||
fontSize: '13px',
|
|
||||||
fontWeight: '800',
|
|
||||||
cursor: 'pointer'
|
|
||||||
},
|
|
||||||
|
|
||||||
row: {
|
|
||||||
width: '100%'
|
|
||||||
},
|
|
||||||
|
|
||||||
itemDivider: {
|
|
||||||
width: '90%',
|
|
||||||
borderBottom: '1px solid grey',
|
|
||||||
margin: '2rem auto'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
58
client/app/components/styles/searchObject.jsx
Normal file
58
client/app/components/styles/searchObject.jsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
|
||||||
|
export default {
|
||||||
|
container: {
|
||||||
|
maxWidth: '95%',
|
||||||
|
margin: '0 auto',
|
||||||
|
minHeight: '230px'
|
||||||
|
},
|
||||||
|
|
||||||
|
title_large: {
|
||||||
|
color: 'black',
|
||||||
|
fontSize: '2em',
|
||||||
|
},
|
||||||
|
|
||||||
|
title_small: {
|
||||||
|
color: 'black',
|
||||||
|
fontSize: '22px',
|
||||||
|
},
|
||||||
|
|
||||||
|
stats_large: {
|
||||||
|
fontSize: '0.8em'
|
||||||
|
},
|
||||||
|
|
||||||
|
stats_small: {
|
||||||
|
marginTop: '5px',
|
||||||
|
fontSize: '0.8em'
|
||||||
|
},
|
||||||
|
|
||||||
|
posterContainer: {
|
||||||
|
float: 'left',
|
||||||
|
zIndex: '3',
|
||||||
|
position: 'relative',
|
||||||
|
marginRight: '30px'
|
||||||
|
},
|
||||||
|
|
||||||
|
posterImage: {
|
||||||
|
border: '2px none',
|
||||||
|
borderRadius: '2px',
|
||||||
|
width: '150px'
|
||||||
|
},
|
||||||
|
|
||||||
|
backgroundImage: {
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
|
||||||
|
summary: {
|
||||||
|
fontSize: '15px',
|
||||||
|
},
|
||||||
|
|
||||||
|
dividerRow: {
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
|
||||||
|
itemDivider: {
|
||||||
|
width: '90%',
|
||||||
|
borderBottom: '1px solid grey',
|
||||||
|
margin: '2rem auto'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,12 +11,13 @@ export default {
|
|||||||
backgroundLargeHeader: {
|
backgroundLargeHeader: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
minHeight: '400px',
|
minHeight: '400px',
|
||||||
backgroundColor: '#011c23',
|
backgroundColor: 'rgb(1, 28, 35)',
|
||||||
|
// backgroundImage: 'radial-gradient(circle, #004c67 0, #005771 120%)',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
marginBottom: '-100px'
|
marginBottom: '-100px'
|
||||||
},
|
},
|
||||||
|
|
||||||
backgroundSmallHeader: {
|
backgroundSmallHeader: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
minHeight: '300px',
|
minHeight: '300px',
|
||||||
backgroundColor: '#011c23',
|
backgroundColor: '#011c23',
|
||||||
@@ -31,7 +32,7 @@ export default {
|
|||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
zIndex: '10',
|
zIndex: '10',
|
||||||
boxShadow: '0 4px 2px black'
|
boxShadow: '0 1px 2px grey',
|
||||||
},
|
},
|
||||||
|
|
||||||
pageTitle: {
|
pageTitle: {
|
||||||
@@ -43,7 +44,7 @@ export default {
|
|||||||
|
|
||||||
pageTitleLargeSpan: {
|
pageTitleLargeSpan: {
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontSize: '3em',
|
fontSize: '4em',
|
||||||
marginTop: '4vh',
|
marginTop: '4vh',
|
||||||
marginBottom: '6vh'
|
marginBottom: '6vh'
|
||||||
},
|
},
|
||||||
@@ -133,70 +134,4 @@ export default {
|
|||||||
color: 'black',
|
color: 'black',
|
||||||
fontSize: '1.4em',
|
fontSize: '1.4em',
|
||||||
},
|
},
|
||||||
|
|
||||||
row: {
|
|
||||||
width: '100%'
|
|
||||||
},
|
|
||||||
|
|
||||||
itemDivider: {
|
|
||||||
width: '90%',
|
|
||||||
borderBottom: '1px solid grey',
|
|
||||||
margin: '1rem auto'
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
pageNavigationBar: {
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
},
|
|
||||||
|
|
||||||
pageNavigationButton: {
|
|
||||||
margin: '0 auto',
|
|
||||||
},
|
|
||||||
|
|
||||||
hvrUnderlineFromCenter: {
|
|
||||||
color: 'white',
|
|
||||||
fontSize: '1em',
|
|
||||||
paddingTop: '12px',
|
|
||||||
marginBottom: '12px',
|
|
||||||
marginLeft: '10px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
display: 'inline-block',
|
|
||||||
verticalAlign: 'middle',
|
|
||||||
WebkitTransform: 'perspective(1px) translateZ(0)',
|
|
||||||
transform: 'perspective(1px) translateZ(0)',
|
|
||||||
boxShadow: '0 0 1px transparent',
|
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden',
|
|
||||||
':before': {
|
|
||||||
content: "",
|
|
||||||
position: 'absolute',
|
|
||||||
zIndex: '-1',
|
|
||||||
left: '50%',
|
|
||||||
right: '50%',
|
|
||||||
bottom: '0',
|
|
||||||
background: '#00d17c',
|
|
||||||
height: '2px',
|
|
||||||
WebkitTransitionProperty: 'left, right',
|
|
||||||
transitionProperty: 'left, right',
|
|
||||||
WebkitTransitionDuration: '0.3s',
|
|
||||||
transitionDuration: '0.3s',
|
|
||||||
WebkitTransitionTimingFunction: 'ease-out',
|
|
||||||
transitionTimingFunction: 'ease-out'
|
|
||||||
},
|
|
||||||
':hover:before': {
|
|
||||||
left: 0,
|
|
||||||
right: 0
|
|
||||||
},
|
|
||||||
'focus:before': {
|
|
||||||
left: 0,
|
|
||||||
right: 0
|
|
||||||
},
|
|
||||||
'active:before': {
|
|
||||||
left: 0,
|
|
||||||
right: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300" rel="stylesheet">
|
||||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
|
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0, user-scalable=0">
|
||||||
<title>seasoned Shows</title>
|
<title>seasoned Shows</title>
|
||||||
</head>
|
</head>
|
||||||
<body style='margin: 0'>
|
<body style='margin: 0'>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"react-burger-menu": "^2.1.6",
|
"react-burger-menu": "^2.1.6",
|
||||||
"react-dom": "^15.5.4",
|
"react-dom": "^15.5.4",
|
||||||
"react-infinite-scroller": "^1.0.15",
|
"react-infinite-scroller": "^1.0.15",
|
||||||
|
"react-interactive": "^0.8.1",
|
||||||
"react-notify-toast": "^0.3.2",
|
"react-notify-toast": "^0.3.2",
|
||||||
"react-redux": "^5.0.6",
|
"react-redux": "^5.0.6",
|
||||||
"react-responsive": "^1.3.4",
|
"react-responsive": "^1.3.4",
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
module: {
|
module: {
|
||||||
loaders: [
|
loaders: [
|
||||||
{ test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
|
{ test: /\.(js|jsx)$/, loader: 'babel-loader', exclude: /node_modules/ },
|
||||||
{ test: /\.jsx$/, loader: 'babel-loader', exclude: /node_modules/ },
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user