Moved old webpage, app & client into .archive folder

This commit is contained in:
2022-08-19 00:44:25 +02:00
parent 851af204ab
commit 0efc109992
66 changed files with 0 additions and 0 deletions

8
.archive/client/.babelrc Normal file
View File

@@ -0,0 +1,8 @@
/*
./.babelrc
*/
{
"presets":[
"es2015", "env", "react"
]
}

58
.archive/client/.gitignore vendored Normal file
View File

@@ -0,0 +1,58 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env

Binary file not shown.

View File

@@ -0,0 +1,25 @@
import React, { Component } from 'react';
import { HashRouter as Router, Route, Switch, IndexRoute } from 'react-router-dom';
import SearchRequest from './components/SearchRequest.jsx';
import AdminComponent from './components/admin/Admin.jsx';
class Root extends Component {
// We need to provide a list of routes
// for our app, and in this case we are
// doing so from a Root component
render() {
return (
<Router>
<Switch>
<Route exact path='/' component={SearchRequest} />
<Route path='/admin/:request' component={AdminComponent} />
<Route path='/admin' component={AdminComponent} />
</Switch>
</Router>
);
}
}
export default Root;

View File

@@ -0,0 +1,41 @@
@font-face {
font-family: "din";
src: url('/app/DIN-Regular-webfont.woff')
}
html {
font-family: 'din', 'Open Sans', sans-serif;
display: inline-block;
color:red;
}
#requestMovieList {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.movie_wrapper {
color:red;
display: flex;
align-content: center;
width: 30%;
background-color: #ffffff;
height: 231px;
margin: 20px;
-webkit-box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
}
.movie_content {
margin-left: 15px;
}
.movie_header {
font-size: 1.6em;
}

View File

@@ -0,0 +1,26 @@
import React from 'react';
export function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i <ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return false;
}
export function setCookie(cname, cvalue, exdays) {
var d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
var expires = "expires="+ d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}

View File

@@ -0,0 +1,63 @@
import React from 'react';
class FetchData extends React.Component {
constructor(props){
super(props)
this.state = {
playing: [],
hei: '1',
intervalId: null,
url: ''
}
}
componentDidMount(){
var that = this;
fetch("https://apollo.kevinmidboe.com/api/v1/plex/playing").then(
function(response){
response.json().then(function(data){
that.setState({
playing: that.state.playing.concat(data.video)
})
})
}
)
}
componentWillUnmount() {
// use intervalId from the state to clear the interval
clearInterval(this.state.intervalId);
}
getPlaying() {
if (this.state.playing.length != 0) {
return this.state.playing.map((playingObj) => {
if (playingObj.type === 'episode') {
console.log('episode')
return ([
<span>{playingObj.title}</span>,
<span>{playingObj.season}</span>,
<span>{playingObj.episode}</span>
])
} else if (playingObj.type === 'movie') {
console.log('movie')
return ([
<span>{playingObj.title}</span>
])
}
})
} else {
return (<span>Nothing playing</span>)
}
}
render(){
return(
<div className="FetchData">{this.getPlaying()}</div>
);
}
}
export default FetchData;

View File

@@ -0,0 +1,266 @@
import React from 'react';
import requestElement from './styles/requestElementStyle.jsx'
import { getCookie } from './Cookie.jsx';
class DropdownList extends React.Component {
constructor(props) {
super(props);
this.state = {
filter: ['all', 'requested', 'downloading', 'downloaded'],
sort: ['requested_date', 'name', 'status', 'requested_by', 'ip', 'user_agent'],
status: ['requested', 'downloading', 'downloaded'],
}
}
render() {
const {elementType, elementId, elementStatus, elementCallback, props} = this.props;
console.log(elementCallback('downloaded'))
switch (elementType) {
case 'status':
return (
<div>HERE</div>
)
}
return (
<div {...props}>
</div>
);
}
}
class RequestElement extends React.Component {
constructor(props) {
super(props);
this.state = {
dropDownState: undefined,
}
}
filterRequestList(requestList, filter) {
if (filter === 'all')
return requestList
if (filter === 'movie' || filter === 'show')
return requestList.filter(item => item.type === filter)
return requestList.filter(item => item.status === filter)
}
sortRequestList(requestList, sort_type, reversed) {
requestList.sort(function(a,b) {
if(a[sort_type] < b[sort_type]) return -1;
if(a[sort_type] > b[sort_type]) return 1;
return 0;
});
if (reversed)
requestList.reverse();
}
userAgent(agent) {
if (agent) {
try {
return agent.split(" ")[1].replace(/[\(\;]/g, '');
}
catch(e) {
return agent;
}
}
return '';
}
updateDropDownState(status) {
if (status !== this.dropDownState) {
this.dropDownState = status;
}
}
ItemsStatusDropdown(id, type, status) {
return (
<div>
<select id="lang"
defaultValue={status}
onChange={event => this.updateDropDownState(event.target.value)}
>
<option value='requested'>Requested</option>
<option value='downloading'>Downloading</option>
<option value='downloaded'>Downloaded</option>
</select>
<button onClick={() => { this.updateRequestedItem(id, type)}}>Update Status</button>
</div>
)
}
updateRequestedItem(id, type) {
console.log(id, type, this.dropDownState);
Promise.resolve()
fetch('https://apollo.kevinmidboe.com/api/v1/plex/request/' + id, {
method: 'PUT',
headers: {
'Content-type': 'application/json',
'authorization': getCookie('token')
},
body: JSON.stringify({
type: type,
status: this.dropDownState,
})
})
.then(response => {
if (response.status !== 200) {
console.log('error');
}
response.json()
.then(data => {
if (data.success === true) {
console.log('UPDATED :', id, ' with ', this.dropDownState)
}
})
})
.catch(error => {
new Error(error);
})
}
createHTMLElement(data, index) {
var posterPath = 'https://image.tmdb.org/t/p/w300' + data.image_path;
return (
<div style={requestElement.wrappingDiv} key={index}>
<img style={requestElement.requestPoster} src={posterPath}></img>
<div style={requestElement.infoDiv}>
<span><b>Name</b>: {data.name} </span><br></br>
<span><b>Year</b>: {data.year}</span><br></br>
<span><b>Type</b>: {data.type}</span><br></br>
<span><b>Status</b>: {data.status}</span><br></br>
<span><b>Address</b>: {data.ip}</span><br></br>
<span><b>Requested Data:</b> {data.requested_date}</span><br></br>
<span><b>Requested By:</b> {data.requested_by}</span><br></br>
<span><b>Agent</b>: { this.userAgent(data.user_agent) }</span><br></br>
</div>
{ this.ItemsStatusDropdown(data.id, data.type, data.status) }
</div>
)
}
render() {
const {requestedElementsList, requestedElementsFilter, requestedElementsSort, props} = this.props;
var filteredRequestedList = this.filterRequestList(requestedElementsList, requestedElementsFilter)
this.sortRequestList(filteredRequestedList, requestedElementsSort.value, requestedElementsSort.reversed)
return (
<div {...props} style={requestElement.bodyDiv}>
{filteredRequestedList.map((requestItem, index) => this.createHTMLElement(requestItem, index))}
</div>
);
}
}
class FetchRequested extends React.Component {
constructor(props){
super(props)
this.state = {
requested_objects: [],
filter: 'all',
sort: {
value: 'requested_date',
reversed: false
},
}
}
componentDidMount(){
Promise.resolve()
fetch('https://apollo.kevinmidboe.com/api/v1/plex/requests/all', {
method: 'GET',
headers: {
'Content-type': 'application/json',
'authorization': getCookie('token')
}
})
.then(response => {
if (response.status !== 200) {
console.log('error');
}
response.json()
.then(data => {
if (data.success === true) {
this.setState({
requested_objects: data.requestedItems
})
}
})
})
.catch(error => {
new Error(error);
})
}
changeFilter(filter) {
this.setState({
filter: filter
})
}
updateSort(sort=null, reverse=false) {
if (sort) {
this.setState({
sort: { value: sort, reversed: reverse }
})
}
else {
this.setState({
sort: { value: this.state.sort.value, reversed: reverse }
})
}
}
render(){
return(
<div>
<select id="lang" onChange={event => this.changeFilter(event.target.value)} value={this.state.value}>
<option value="all">All</option>
<option value="requested">Requested</option>
<option value="downloading">Downloading</option>
<option value="downloaded">Downloaded</option>
<option value='movie'>Movies</option>
<option value='show'>Shows</option>
</select>
<select id="lang" onChange={event => this.updateSort(event.target.value)} value={this.state.value}>
<option value='requested_date'>Date</option>
<option value='name'>Title</option>
<option value='status'>Status</option>
<option value='requested_by'>Requested By</option>
<option value='ip'>Address</option>
<option value='user_agent'>Agent</option>
</select>
<button onClick={() => {this.updateSort(null, !this.state.sort.reversed)}}>Reverse</button>
<RequestElement
requestedElementsList={this.state.requested_objects}
requestedElementsFilter={this.state.filter}
requestedElementsSort={this.state.sort}
/>
</div>
)
}
}
export default FetchRequested;

View File

@@ -0,0 +1,11 @@
import React from 'react'
import { Link } from 'react-router-dom'
// The Header creates links that can be used to navigate
// between routes.
const Header = () => (
<header>
</header>
)
export default Header

View File

@@ -0,0 +1,44 @@
import React from 'react';
class ListStrays extends React.Component {
constructor(props){
super(props)
this.state = {
strays: [],
hei: '1'
}
}
componentDidMount(){
var that = this;
fetch('https://apollo.kevinmidboe.com/api/v1/seasoned/all').then(
function(response){
response.json().then(function(data){
// console.log(data);
that.setState({
strays: that.state.strays.concat(data)
})
})
}
)
}
render(){
return(
<div className="ListStrays">
{this.state.strays.map((strayObj) => {
if (strayObj.verified == 0) {
var url = "https://kevinmidboe.com/seasoned/verified.html?id=" + strayObj.id;
return ([
<span key={strayObj.id}>{strayObj.name}</span>,
<a href={url}>{strayObj.id}</a>
])
}
})}
</div>
)
}
}
export default ListStrays;

View File

@@ -0,0 +1,10 @@
// components/NotFound.js
import React from 'react';
const NotFound = () =>
<div>
<h3>404 page not found</h3>
<p>We are sorry but the page you are looking for does not exist.</p>
</div>
export default NotFound;

View File

@@ -0,0 +1,126 @@
import React from 'react';
import Notifications, {notify} from 'react-notify-toast';
// StyleComponents
import searchObjectCSS from './styles/searchObject.jsx';
import buttonsCSS from './styles/buttons.jsx';
import InfoButton from './buttons/InfoButton.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_path;
this.background = object.background_path;
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/w185' + 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>&#x0002B; Request</span>
</Interactive>;
}
// 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} | Type: {this.type}
</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}
<InfoButton id={this.id} type={this.type} />
</div>
</div>
<MediaQuery maxWidth={600}>
<br />
</MediaQuery>
<div style={searchObjectCSS.dividerRow}>
<div style={searchObjectCSS.itemDivider}></div>
</div>
</div>
)
}
}
export default SearchObject;

View File

@@ -0,0 +1,464 @@
import React from 'react';
import URI from 'urijs';
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 { getCookie } from './Cookie.jsx';
var MediaQuery = require('react-responsive');
// TODO add option for searching multi, movies or tv shows
class SearchRequest extends React.Component {
constructor(props){
super(props)
// Constructor with states holding the search query and the element of reponse.
this.state = {
lastApiCallURI: '',
searchQuery: '',
responseMovieList: null,
movieFilter: false,
showFilter: false,
discoverType: '',
page: 1,
resultHeader: '',
loadResults: false,
scrollHasMore: true,
loading: false,
}
this.allowedListTypes = ['discover', 'popular', 'nowplaying', 'upcoming']
this.baseUrl = 'https://apollo.kevinmidboe.com/api/v1/tmdb/list';
// this.baseUrl = 'http://localhost:31459/api/v1/tmdb/list';
this.searchUrl = 'https://apollo.kevinmidboe.com/api/v1/plex/request';
// this.searchUrl = 'http://localhost:31459/api/v1/plex/request';
}
componentWillMount(){
var that = this;
// this.setState({responseMovieList: null})
this.resetPageNumber();
this.state.loadResults = true;
this.fetchTmdbList(this.allowedListTypes[Math.floor(Math.random()*this.allowedListTypes.length)]);
}
// Handles all errors of the response of a fetch call
handleErrors(response) {
if (!response.ok)
throw Error(response.status);
return response;
}
handleQueryError(response) {
if (!response.ok) {
if (response.status === 404) {
this.setState({
responseMovieList: <h1>Nothing found for search query: { this.findQueryInURI(uri) }</h1>
})
}
console.log('handleQueryError: ', error);
}
return response;
}
// Unpacks the query value of a uri
findQueryValueInURI(uri) {
let uriSearchValues = uri.query(true);
let queryValue = uriSearchValues['query']
return queryValue;
}
// Unpacks the page value of a uri
findPageValueInURI(uri) {
let uriSearchValues = uri.query(true);
let queryValue = uriSearchValues['page']
return queryValue;
}
resetPageNumber() {
this.state.page = 1;
}
setLoading(value) {
this.setState({
loading: value
});
}
// Test this by calling missing endpoint or 404 query and see what code
// and filter the error message based on the code.
// Calls a uri and returns the response as json
callURI(uri, method, data={}) {
return fetch(uri, {
method: method,
headers: new Headers({
'Content-Type': 'application/json',
'authorization': getCookie('token'),
'loggedinuser': getCookie('loggedInUser'),
})
})
.then(response => { return response })
.catch((error) => {
throw Error(error);
});
}
// Saves the input string as a h1 element in responseMovieList state
fillResponseMovieListWithError(msg) {
this.setState({
responseMovieList: <h1>{ msg }</h1>
})
}
// 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
callSearchFillMovieList(uri) {
Promise.resolve()
.then(() => this.callURI(uri, 'GET'))
.then(response => {
// If we get a error code for the request
if (!response.ok) {
if (response.status === 404) {
if (this.findPageValueInURI(new URI(response.url)) > 1) {
this.state.scrollHasMore = false;
console.log(this.state.scrollHasMore)
return null
let returnMessage = 'this is the return mesasge than will never be delivered'
let theSecondReturnMsg = 'this is the second return messag ethat will NEVE be delivered'
}
else {
let errorMsg = 'Nothing found for the search query: ' + this.findQueryValueInURI(uri);
this.fillResponseMovieListWithError(errorMsg)
}
}
else {
let errorMsg = 'Error fetching query from server ' + this.response.status;
this.fillResponseMovieListWithError(errorMsg)
}
}
// Convert to json and update the state of responseMovieList with the results of the api call
// mapped as a SearchObject.
response.json()
.then(responseData => {
if (this.state.page === 1) {
this.setState({
responseMovieList: responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index)),
lastApiCallURI: uri // Save the value of the last sucessfull api call
})
} else {
let responseMovieObjects = responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index));
let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects);
this.setState({
responseMovieList: growingReponseMovieObjectList,
lastApiCallURI: uri // Save the value of the last sucessfull api call
})
}
})
.catch((error) => {
console.log('CallSearchFillMovieList: ', error)
})
})
.catch((error) => {
console.log('Something went wrong when fetching query.', error)
})
}
callListFillMovieList(uri) {
// Write loading animation
Promise.resolve()
.then(() => this.callURI(uri, 'GET', undefined))
.then(response => {
// If we get a error code for the request
if (!response.ok) {
if (response.status === 404) {
let errorMsg = 'List not found';
this.fillResponseMovieListWithError(errorMsg)
}
else {
let errorMsg = 'Error fetching list from server ' + this.response.status;
this.fillResponseMovieListWithError(errorMsg)
}
}
// Convert to json and update the state of responseMovieList with the results of the api call
// mapped as a SearchObject.
response.json()
.then(responseData => {
if (this.state.page === 1) {
this.setState({
responseMovieList: responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index)),
lastApiCallURI: uri // Save the value of the last sucessfull api call
})
} else {
let responseMovieObjects = responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index));
let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects);
this.setState({
responseMovieList: growingReponseMovieObjectList,
lastApiCallURI: uri // Save the value of the last sucessfull api call
})
}
})
})
.catch((error) => {
console.log('Something went wrong when fetching query.', error)
})
}
searchSeasonedRequest() {
this.state.resultHeader = 'Search result for: ' + this.state.searchQuery;
// Build uri with the url for searching requests
var uri = new URI(this.searchUrl);
// Add input of search query and page count to the uri payload
uri = uri.search({ 'query': this.state.searchQuery, 'page': this.state.page });
if (this.state.showFilter)
uri = uri.addSearch('type', 'show');
else
if (this.state.movieFilter)
uri = uri.addSearch('type', 'movie')
// Send uri to call and fill the response list with movie/show objects
this.callSearchFillMovieList(uri);
}
fetchTmdbList(tmdbListType) {
console.log(tmdbListType)
// Check if it is a whitelisted list, this should be replaced with checking if the return call is 500
if (this.allowedListTypes.indexOf(tmdbListType) === -1)
throw Error('Invalid discover type: ' + tmdbListType);
this.state.responseMovieList = []
// Captialize the first letter of and save the discoverQueryType to resultHeader state.
this.state.resultHeader = tmdbListType.toLowerCase().replace(/\b[a-z]/g, function(letter) {
return letter.toUpperCase();
});
// Build uri with the url for searching requests
var uri = new URI(this.baseUrl);
uri.segment(tmdbListType);
// Add input of search query and page count to the uri payload
uri = uri.search({ 'page': this.state.page });
if (this.state.showFilter)
uri = uri.addSearch('type', 'show');
// Send uri to call and fill the response list with movie/show objects
this.callListFillMovieList(uri);
}
// Updates the internal state of the query search field.
updateQueryState(event){
this.setState({
searchQuery: event.target.value
});
}
// For checking if the enter key was pressed in the search field.
_handleQueryKeyPress(e) {
if (e.key === 'Enter') {
// this.fetchQuery();
// Reset page number for a new search
this.resetPageNumber();
this.searchSeasonedRequest();
}
}
// When called passes the variable to SearchObject and calls it's interal function for
// generating the wanted HTML
createMovieObjects(item, index) {
let movie = new SearchObject(item);
return movie.getElement(index);
}
toggleFilter(filterType) {
if (filterType == 'movies') {
this.setState({
movieFilter: !this.state.movieFilter
})
console.log(this.state.movieFilter);
}
else if (filterType == 'shows') {
this.setState({
showFilter: !this.state.showFilter
})
console.log(this.state.showFilter);
}
}
pageBackwards() {
if (this.state.page > 1) {
let pageNumber = this.state.page - 1;
let uri = this.state.lastApiCallURI;
// Augment the page number of the uri with a callback
uri.search(function(data) {
data.page = pageNumber;
});
// Call the api with the new uri
this.callSearchFillMovieList(uri);
// Update state of our page number after the call is done
this.state.page = pageNumber;
}
}
// TODO need to get total page number and save in a state to not overflow
pageForwards() {
// Wrap this in the check
let pageNumber = this.state.page + 1;
let uri = this.state.lastApiCallURI;
// Augment the page number of the uri with a callback
uri.search(function(data) {
data.page = pageNumber;
});
// Call the api with the new uri
this.callSearchFillMovieList(uri);
// Update state of our page number after the call is done
this.state.page = pageNumber;
}
movieToggle() {
if (this.state.movieFilter)
return <span style={searchRequestCSS.searchFilterActive}
className="search_category hvrUnderlineFromCenter"
onClick={() => {this.toggleFilter('movies')}}
id="category_active">Movies</span>
else
return <span style={searchRequestCSS.searchFilterNotActive}
className="search_category hvrUnderlineFromCenter"
onClick={() => {this.toggleFilter('movies')}}
id="category_active">Movies</span>
}
showToggle() {
if (this.state.showFilter)
return <span style={searchRequestCSS.searchFilterActive}
className="search_category hvrUnderlineFromCenter"
onClick={() => {this.toggleFilter('shows')}}
id="category_active">TV Shows</span>
else
return <span style={searchRequestCSS.searchFilterNotActive}
className="search_category hvrUnderlineFromCenter"
onClick={() => {this.toggleFilter('shows')}}
id="category_active">TV Shows</span>
}
render(){
const loader = <div className="loader">Loading ...<br></br></div>;
return(
<InfiniteScroll
pageStart={0}
loadMore={this.pageForwards.bind(this)}
hasMore={this.state.scrollHasMore}
loader={<Loading />}
initialLoad={this.state.loadResults}>
<MediaQuery minWidth={600}>
<div style={searchRequestCSS.body}>
<div className='backgroundHeader' style={searchRequestCSS.backgroundLargeHeader}>
<div className='pageTitle' style={searchRequestCSS.pageTitle}>
<span style={searchRequestCSS.pageTitleLargeSpan}>Request new content for plex</span>
</div>
<div style={searchRequestCSS.searchLargeContainer}>
<span style={searchRequestCSS.searchIcon}><i className="fa fa-search"></i></span>
<input style={searchRequestCSS.searchLargeBar} type="text" id="search" placeholder="Search for new content..."
onKeyPress={(event) => this._handleQueryKeyPress(event)}
onChange={event => this.updateQueryState(event)}
value={this.state.searchQuery}/>
</div>
</div>
<div id='requestMovieList' ref='requestMovieList' style={searchRequestCSS.requestWrapper}>
<div style={{marginLeft: '30px'}}>
<div style={searchRequestCSS.resultLargeHeader}>{this.state.resultHeader}</div>
<span style={{content: '', display: 'block', width: '2em', borderTop: '2px solid #000,'}}></span>
</div>
<br></br>
{this.state.responseMovieList}
</div>
</div>
</MediaQuery>
<MediaQuery maxWidth={600}>
<div style={searchRequestCSS.body}>
<div className='backgroundHeader' style={searchRequestCSS.backgroundSmallHeader}>
<div className='pageTitle' style={searchRequestCSS.pageTitle}>
<span style={searchRequestCSS.pageTitleSmallSpan}>Request new content</span>
</div>
<div className='box' style={searchRequestCSS.box}>
<div style={searchRequestCSS.searchSmallContainer}>
<span style={searchRequestCSS.searchIcon}><i className="fa fa-search"></i></span>
<input style={searchRequestCSS.searchSmallBar} type="text" id="search" placeholder="Search for new content..."
onKeyPress={(event) => this._handleQueryKeyPress(event)}
onChange={event => this.updateQueryState(event)}
value={this.state.searchQuery}/>
</div>
</div>
</div>
<div id='requestMovieList' ref='requestMovieList' style={searchRequestCSS.requestWrapper}>
<span style={searchRequestCSS.resultSmallHeader}>{this.state.resultHeader}</span>
<br></br><br></br>
{this.state.responseMovieList}
</div>
</div>
</MediaQuery>
</InfiniteScroll>
)
}
// <form style={searchRequestCSS.controls}>
// <label style={searchRequestCSS.withData}>
// <div style={searchRequestCSS.sortOptions}>Discover</div>
// </label>
// </form>
// <form style={searchRequestCSS.controls}>
// <label style={searchRequestCSS.withData}>
// <select style={searchRequestCSS.sortOptions}>
// <option value="discover">All</option>
// <option value="nowplaying">Movies</option>
// <option value="nowplaying">TV Shows</option>
// </select>
// </label>
// </form>
}
export default SearchRequest;

View File

@@ -0,0 +1,92 @@
import React from 'react';
import LoginForm from './LoginForm/LoginForm.jsx';
import { Provider } from 'react-redux';
import store from '../redux/store.jsx';
import { getCookie } from '../Cookie.jsx';
import { fetchJSON } from '../http.jsx';
import Sidebar from './Sidebar.jsx';
import AdminRequestInfo from './AdminRequestInfo.jsx';
import adminCSS from '../styles/adminComponent.jsx'
class AdminComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
requested_objects: '',
}
this.updateHandler = this.updateHandler.bind(this)
}
// Fetches all requested elements and updates the state with response
componentWillMount() {
this.fetchRequestedItems()
}
fetchRequestedItems() {
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/requests/all', 'GET')
.then(result => {
this.setState({
requested_objects: result.results.reverse()
})
})
}
updateHandler() {
this.fetchRequestedItems()
}
// Displays loginform if not logged in and passes response from
// api call to sidebar and infoPanel through props
verifyLoggedIn() {
const logged_in = getCookie('logged_in');
if (!logged_in) {
return <LoginForm />
}
let selectedRequest = undefined;
let listItemSelected = undefined;
const requestParam = this.props.match.params.request;
if (requestParam && this.state.requested_objects !== '') {
selectedRequest = this.state.requested_objects[requestParam]
listItemSelected = requestParam;
}
return (
<div>
<div style={adminCSS.selectedObjectPanel}>
<AdminRequestInfo
selectedRequest={selectedRequest}
listItemSelected={listItemSelected}
updateHandler = {this.updateHandler}
/>
</div>
<div style={adminCSS.sidebar}>
<Sidebar
requested_objects={this.state.requested_objects}
listItemSelected={listItemSelected}
/>
</div>
</div>
)
}
render() {
return (
<Provider store={store}>
{ this.verifyLoggedIn() }
</Provider>
)
}
}
export default AdminComponent;

View File

@@ -0,0 +1,218 @@
import React, { Component } from 'react';
import { fetchJSON } from '../http.jsx';
import PirateSearch from './PirateSearch.jsx'
// No in use!
import InfoButton from '../buttons/InfoButton.jsx';
// Stylesheets
import requestInfoCSS from '../styles/adminRequestInfo.jsx'
import buttonsCSS from '../styles/buttons.jsx';
String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1);
}
class AdminRequestInfo extends Component {
constructor() {
super();
this.state = {
statusValue: '',
movieInfo: undefined,
expandedSummary: false,
}
this.requestInfo = '';
}
componentWillReceiveProps(props) {
this.requestInfo = props.selectedRequest;
this.state.statusValue = this.requestInfo.status;
this.state.expandedSummary = false;
this.fetchIteminfo()
}
userAgent(agent) {
if (agent) {
try {
return agent.split(" ")[1].replace(/[\(\;]/g, '');
}
catch(e) {
return agent;
}
}
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.props.updateHandler()
})
}
generateStatusIndicator(status) {
switch (status) {
case 'requested':
// Yellow
return 'linear-gradient(to right, rgb(63, 195, 243) 0, rgb(63, 195, 243) 10px, #fff 4px, #fff 100%) no-repeat'
case 'downloading':
// Blue
return 'linear-gradient(to right, rgb(255, 225, 77) 0, rgb(255, 225, 77) 10px, #fff 4px, #fff 100%) no-repeat'
case 'downloaded':
// Green
return 'linear-gradient(to right, #39aa56 0, #39aa56 10px, #fff 4px, #fff 100%) no-repeat'
default:
return 'linear-gradient(to right, grey 0, grey 10px, #fff 4px, #fff 100%) no-repeat'
}
}
generateTypeIcon(type) {
if (type === 'show')
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"></rect><polyline points="17 2 12 7 7 2"></polyline></svg>
)
else if (type === 'movie')
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></svg>
)
else
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12" y2="16"></line></svg>
)
}
toggleSummmaryLength() {
this.setState({
expandedSummary: !this.state.expandedSummary
})
}
generateSummary() {
// { this.state.movieInfo != undefined ? this.state.movieInfo.summary : 'Loading...' }
const info = this.state.movieInfo;
if (info !== undefined) {
const summary = this.state.movieInfo.summary
const summary_short = summary.slice(0, 180);
return (
<div>
<span><b>Matched: </b> {String(info.matchedInPlex)}</span> <br/>
<span><b>Rating: </b> {info.rating}</span> <br/>
<span><b>Popularity: </b> {info.popularity}</span> <br/>
{
(summary.length > 180 && this.state.expandedSummary === false) ?
<span><b>Summary: </b> { summary_short }<span onClick = {() => this.toggleSummmaryLength()}>... <span style={{color: 'blue', cursor: 'pointer'}}>Show more</span></span></span>
:
<span><b>Summary: </b> { summary }<span onClick = {() => this.toggleSummmaryLength()}><span style={{color: 'blue', cursor: 'pointer'}}> Show less</span></span></span>
}
</div>
)
} else {
return <span>Loading...</span>
}
}
requested_by_user(request_user) {
if (request_user === 'NULL')
return undefined
return (
<span><b>Requested by:</b> {request_user}</span>
)
}
fetchIteminfo() {
const itemID = this.requestInfo.id;
const type = this.requestInfo.type;
fetchJSON('https://apollo.kevinmidboe.com/api/v1/tmdb/' + itemID +'&type='+type, 'GET')
.then((response) => {
console.log('Response, getInfo:', response)
this.setState({
movieInfo: response
});
console.log(this.state.movieInfo)
})
}
displayInfo() {
const request = this.props.selectedRequest;
if (request) {
requestInfoCSS.info.background = this.generateStatusIndicator(request.status);
return (
<div style={requestInfoCSS.wrapper}>
<div style={requestInfoCSS.stick}>
<span style={requestInfoCSS.title}> {request.title} {request.year}</span>
<span style={{marginLeft: '2em'}}>
<span style={requestInfoCSS.type_icon}>{this.generateTypeIcon(request.type)}</span>
{/*<span style={style.type_text}>{request.type.capitalize()}</span> <br />*/}
</span>
</div>
<div style={requestInfoCSS.info}>
<div style={requestInfoCSS.info_poster}>
<img src={'https://image.tmdb.org/t/p/w185' + request.poster_path} style={requestInfoCSS.image} alt='Movie poster image'></img>
</div>
<div style={requestInfoCSS.info_request}>
<h3 style={requestInfoCSS.info_request_header}>Request info</h3>
<span><b>status:</b>{ request.status }</span><br />
<span><b>ip:</b>{ request.ip }</span><br />
<span><b>user_agent:</b>{ this.userAgent(request.user_agent) }</span><br />
<span><b>request_date:</b>{ request.requested_date}</span><br />
{ this.requested_by_user(request.requested_by) }<br />
{ this.generateStatusDropdown() }<br />
</div>
<div style={requestInfoCSS.info_movie}>
<h3 style={requestInfoCSS.info_movie_header}>Movie info</h3>
{ this.generateSummary() }
</div>
</div>
<PirateSearch style={requestInfoCSS.search} name={request.title} />
</div>
)
}
}
render() {
return (
<div>{this.displayInfo()}</div>
);
}
}
export default AdminRequestInfo;

View File

@@ -0,0 +1,66 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { login } from '../../redux/reducer.jsx';
class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {};
this.onSubmit = this.onSubmit.bind(this);
}
render() {
let {email, password} = this.state;
let {isLoginPending, isLoginSuccess, loginError} = this.props;
return (
<form name="loginForm" onSubmit={this.onSubmit}>
<div className="form-group-collection">
<div className="form-group">
<label>Email:</label>
<input type="" name="email" onChange={e => this.setState({email: e.target.value})} value={email}/>
</div>
<div className="form-group">
<label>Password:</label>
<input type="password" name="password" onChange={e => this.setState({password: e.target.value})} value={password}/>
</div>
</div>
<input type="submit" value="Login" />
<div className="message">
{ isLoginPending && <div>Please wait...</div> }
{ isLoginSuccess && <div>Success.</div> }
{ loginError && <div>{loginError.message}</div> }
</div>
</form>
)
}
onSubmit(e) {
e.preventDefault();
let { email, password } = this.state;
this.props.login(email, password);
this.setState({
email: '',
password: ''
});
}
}
const mapStateToProps = (state) => {
return {
isLoginPending: state.isLoginPending,
isLoginSuccess: state.isLoginSuccess,
loginError: state.loginError
};
}
const mapDispatchToProps = (dispatch) => {
return {
login: (email, password) => dispatch(login(email, password))
};
}
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);

View File

@@ -0,0 +1,95 @@
import React, { Component } from 'react';
import { fetchJSON } from '../http.jsx';
// Components
import TorrentTable from './TorrentTable.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 {
constructor() {
super();
this.state = {
torrentResponse: undefined,
name: '',
loading: null,
showButton: true,
}
}
componentWillReceiveProps(props) {
if (props.name != this.state.name) {
this.setState({
torrentResponse: undefined,
showButton: true,
})
}
}
searchTheBay() {
const query = this.props.name;
const type = this.props.type;
this.setState({
showButton: false,
loading: <Loading />,
})
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
// fetchJSON('http://localhost:31459/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
.then((response) => {
console.log('this is the first response: ', response)
if (response.success === true) {
this.setState({
torrentResponse: response.torrents,
loading: null,
})
}
else {
console.error(response.message)
}
})
.catch((error) => {
console.error(error);
this.setState({
showButton: true,
})
})
}
render() {
btnStylesheet.submit.top = '50%'
btnStylesheet.submit.position = 'absolute'
btnStylesheet.submit.marginLeft = '-75px'
return (
<div>
{ this.state.showButton ?
<div style={{textAlign:'center'}}>
<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>
: null }
{ this.state.loading }
<TorrentTable response={this.state.torrentResponse} />
</div>
)
}
}
export default PirateSearch

View File

@@ -0,0 +1,247 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import Interactive from 'react-interactive';
import sidebarCSS from '../styles/adminSidebar.jsx'
class SidebarComponent extends Component {
constructor(props){
super(props)
// Constructor with states holding the search query and the element of reponse.
this.state = {
filterValue: '',
filterQuery: '',
requestItemsToBeDisplayed: [],
listItemSelected: '',
height: '0',
}
this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
}
// Where we wait for api response to be delivered from parent through props
componentWillReceiveProps(props) {
this.state.listItemSelected = props.listItemSelected;
this.displayRequestedElementsInfo(props.requested_objects);
}
componentDidMount() {
this.updateWindowDimensions();
window.addEventListener('resize', this.updateWindowDimensions);
}
componentWillUnmount() {
window.removeEventListener('resize', this.updateWindowDimensions);
}
updateWindowDimensions() {
this.setState({ height: window.innerHeight });
}
// 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.title.toLowerCase().indexOf(query.toLowerCase()) != -1)
return this.generateListElements(index, item);
})
this.setState({
requestItemsToBeDisplayed: filteredByQuery,
filterQuery: query,
});
}
generateFilterSearch() {
return (
<div style={sidebarCSS.searchSidebar}>
<div style={sidebarCSS.searchInner}>
<input
type="text"
id="search"
style={sidebarCSS.searchTextField}
placeholder="Search requested items"
onChange={event => this.updateFilterQuery(event)}
value={this.state.filterQuery}/>
<span>
<svg id="icon-search" style={sidebarCSS.searchIcon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15">
<g id="search">
<circle style={sidebarCSS.searchSVGIcon} cx="6.055" cy="5.805" r="5.305"></circle>
<path style={sidebarCSS.searchSVGIcon} d="M9.847 9.727l4.166 4.773"></path>
</g>
</svg>
</span>
</div>
</div>
)
}
generateNav() {
let filterValue = this.state.filterValue;
return (
<nav style={sidebarCSS.sidebar_navbar_underline}>
<ul style={sidebarCSS.ulFilterSelectors}>
<li>
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('all') }>All</span>
{ (filterValue === 'all' || filterValue === '') && <span style={sidebarCSS.spanFilterSelectors}></span> }
</li>
<li>
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('requested') }>Requested</span>
{ filterValue === 'requested' && <span style={sidebarCSS.spanFilterSelectors}></span> }
</li>
<li>
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('downloading') }>Downloading</span>
{ filterValue === 'downloading' && <span style={sidebarCSS.spanFilterSelectors}></span> }
</li>
<li>
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('downloaded') }>Downloaded</span>
{ filterValue === 'downloaded' && <span style={sidebarCSS.spanFilterSelectors}></span> }
</li>
</ul>
</nav>
)
}
generateBody(cards) {
let style = sidebarCSS.ulCard;
style.maxHeight = this.state.height - 160;
return (
<ul style={style}>
{ cards }
</ul>
)
}
generateListElements(index, item) {
let statusBar;
switch (item.status) {
case 'requested':
// Yellow
statusBar = { background: 'linear-gradient(to right, rgb(63, 195, 243) 0, rgb(63, 195, 243) 4px, #fff 4px, #fff 100%) no-repeat' }
break;
case 'downloading':
// Blue
statusBar = { background: 'linear-gradient(to right, rgb(255, 225, 77) 0, rgb(255, 225, 77) 4px, #fff 4px, #fff 100%) no-repeat' }
break;
case 'downloaded':
// Green
statusBar = { background: 'linear-gradient(to right, #39aa56 0, #39aa56 4px, #fff 4px, #fff 100%) no-repeat' }
break;
default:
statusBar = { background: 'linear-gradient(to right, grey 0, grey 4px, #fff 4px, #fff 100%) no-repeat' }
}
statusBar.listStyleType = 'none';
return (
<Link style={sidebarCSS.link} to={{ pathname: '/admin/'+String(index)}} key={index}>
<li style={statusBar}>
<Interactive
as='div'
style={ (index != this.state.listItemSelected) ? sidebarCSS.card : sidebarCSS.cardSelected }
hover={sidebarCSS.cardSelected}
focus={sidebarCSS.cardSelected}
active={sidebarCSS.cardSelected}>
<h2 style={sidebarCSS.titleCard}>
<span>{ item.title }</span>
</h2>
<p style={sidebarCSS.pCard}>
<span>Requested:
<time>
&nbsp;{ this.convertDateToDaysSince(item.requested_date) }
</time>
</span>
</p>
</Interactive>
</li>
</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: this.generateBody(requestedElement)
})
}
render() {
// if (typeof InstallTrigger !== 'undefined')
// bodyCSS.width = '-moz-min-content';
return (
<div>
<h1 style={sidebarCSS.header}>Requested items</h1>
{ this.generateFilterSearch() }
{ this.generateNav() }
<div key='requestedTable' style={sidebarCSS.body}>
{ this.state.requestItemsToBeDisplayed }
</div>
</div>
);
}
}
export default SidebarComponent;

View File

@@ -0,0 +1,209 @@
import React, { Component } from 'react';
import { fetchJSON } from '../http.jsx';
import torrentTableCSS from '../styles/adminTorrentTable.jsx';
class TorrentTable extends Component {
constructor() {
super();
this.state = {
torrentResponse: [],
listElements: undefined,
showTable: false,
filterQuery: '',
sortValue: 'name',
sortDesc: true,
}
this.UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
}
componentWillReceiveProps(props) {
if (props.response !== undefined && props.response !== this.state.torrentResponse) {
console.log('not called', props)
this.setState({
torrentResponse: props.response,
showTable: true,
})
} else {
this.setState({
showTable: false,
})
}
}
// BORROWED FROM GITHUB user sindresorhus
// Link to repo: https://github.com/sindresorhus/pretty-bytes
convertSizeToHumanSize(num) {
if (!Number.isFinite(num)) {
return num
// throw new TypeError(`Expected a finite number, got ${typeof num}: ${num}`);
}
const neg = num < 0;
if (neg) {
num = -num;
}
if (num < 1) {
return (neg ? '-' : '') + num + ' B';
}
const exponent = Math.min(Math.floor(Math.log10(num) / 3), this.UNITS.length - 1);
const numStr = Number((num / Math.pow(1000, exponent)).toPrecision(3));
const unit = this.UNITS[exponent];
return (neg ? '-' : '') + numStr + ' ' + unit;
}
convertHumanSizeToBytes(string) {
const [numStr, unit] = string.split(' ');
if (this.UNITS.indexOf(unit) === -1) {
return string
}
const exponent = this.UNITS.indexOf(unit) * 3
return numStr * (Math.pow(10, exponent))
}
sendToDownload(magnet) {
const apiData = {
magnet: magnet,
}
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/add', 'POST', apiData)
// fetchJSON('http://localhost:31459/api/v1/pirate/add', 'POST', apiData)
.then((response) => {
console.log('Response, addMagnet: ', response)
// TODO Display the feedback in a notification component (text, status)
})
}
// 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.response.map((item, index) => {
if (item.name.toLowerCase().indexOf(query.toLowerCase()) != -1)
return item
})
this.setState({
torrentResponse: filteredByQuery,
filterQuery: query,
});
}
sortTable(col) {
let direction = this.state.sortDesc;
if (col === this.state.sortValue)
direction = !direction;
else
direction = true
let sortedItems = this.state.torrentResponse.sort((a,b) => {
// This is so we also can sort string that only contain numbers
let valueA = isNaN(a[col]) ? a[col] : parseInt(a[col])
let valueB = isNaN(b[col]) ? b[col] : parseInt(b[col])
valueA = (col == 'size') ? this.convertHumanSizeToBytes(valueA) : valueA
valueB = (col == 'size') ? this.convertHumanSizeToBytes(valueB) : valueB
if (direction)
return valueA<valueB? 1:valueA>valueB?-1:0;
else
return valueA>valueB? 1:valueA<valueB?-1:0;
})
this.setState({
torrentResponse: sortedItems,
sortDesc: direction,
sortValue: col,
})
}
generateFilterSearch() {
return (
<div style={torrentTableCSS.searchSidebar}>
<div style={torrentTableCSS.searchInner}>
<input
type="text"
id="search"
style={torrentTableCSS.searchTextField}
placeholder="Filter torrents by query"
onChange={event => this.updateFilterQuery(event)}
value={this.state.filterQuery}/>
<span>
<svg id="icon-search" style={torrentTableCSS.searchIcon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15">
<g id="search">
<circle style={torrentTableCSS.searchSVGIcon} cx="6.055" cy="5.805" r="5.305"></circle>
<path style={torrentTableCSS.searchSVGIcon} d="M9.847 9.727l4.166 4.773"></path>
</g>
</svg>
</span>
</div>
</div>
)
}
generateListElements() {
let listElements = this.state.torrentResponse.map((item, index) => {
if (item !== undefined) {
let title = item.name
let size = this.convertSizeToHumanSize(item.size)
return (
<tr key={index} style={torrentTableCSS.bodyCol}>
<td>{ item.name }</td>
<td>{ item.uploader }</td>
<td>{ size }</td>
<td>{ item.seed }</td>
<td><button onClick = { event => this.sendToDownload(item.magnet) }>Send to download</button></td>
</tr>
)
}
})
return listElements
}
render() {
return (
<div style= { this.state.showTable ? null : {display: 'none'}}>
{ this.generateFilterSearch() }
<table style={torrentTableCSS.table} cellSpacing="0" cellPadding="0">
<thead>
<tr>
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('name') }>
Title
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'name' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
</th>
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('uploader') }>
Uploader
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'uploader' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
</th>
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('size') }>
Size
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'size' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
</th>
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('seed') }>
Seeds
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'seed' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
</th>
<th style={torrentTableCSS.col}>Magnet</th>
</tr>
</thead>
<tbody>
{this.generateListElements()}
</tbody>
</table>
</div>
)
}
}
export default TorrentTable;

View File

@@ -0,0 +1,52 @@
import React, { Component } from 'react';
import Interactive from 'react-interactive';
import buttonsCSS from '../styles/buttons.jsx';
class InfoButton extends Component {
constructor(props) {
super(props);
if (props) {
this.state = {
id: props.id,
type: props.type,
}
}
}
componentWillReceiveProps(props) {
this.setState({
id: props.id,
type: props.type,
})
}
getTMDBLink() {
const id = this.state.id;
const type = this.state.type;
if (type === 'movie')
return 'https://www.themoviedb.org/movie/' + id
else if (type === 'show')
return 'https://www.themoviedb.org/tv/' + id
}
render() {
return (
<a href={this.getTMDBLink()}>
<Interactive
as='button'
hover={buttonsCSS.info_hover}
focus={buttonsCSS.info_hover}
style={buttonsCSS.info}>
<span>More info</span>
</Interactive>
</a>
);
}
}
export default InfoButton;

View File

@@ -0,0 +1,22 @@
import React from 'react';
class RequestButton extends React.Component {
constructor() {
super();
this.state = {textColor: 'white'};
}
render() {
return (
<Text
style={{color: this.state.textColor}}
onEnter={() => this.setState({textColor: 'red'})}
onExit={() => this.setState({textColor: 'white'})}>
This text will turn red when you look at it.
</Text>
);
}
}
export default RequestButton;

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { getCookie } from './Cookie.jsx';
// class http {
// dispatch(obj) {
// console.log(obj);
// }
function checkStatus(response) {
const hasError = (response.status < 200 || response.status >= 300)
if (hasError) {
throw response.text();
}
return response;
}
function parseJSON(response) { return response.json(); }
// *
// * Retrieve search results from tmdb with added seasoned information.
// * @param {String} uri query you want to search for
// * @param {Number} page representing pagination of results
// * @returns {Promise} succeeds if results were found
// fetchSearch(uri) {
// fetch(uri, {
// method: 'GET',
// headers: {
// 'authorization': getCookie('token')
// },
// })
// .then(response => {
// });
// }
// }
// export default http;
export function fetchJSON(url, method, data) {
return fetch(url, {
method: method,
headers: new Headers({
'Content-Type': 'application/json',
'authorization': getCookie('token'),
'loggedinuser': getCookie('loggedInUser'),
}),
body: JSON.stringify(data)
}).then(checkStatus).then(parseJSON);
}

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

View File

@@ -0,0 +1,109 @@
import { setCookie } from '../Cookie.jsx';
const SET_LOGIN_PENDING = 'SET_LOGIN_PENDING';
const SET_LOGIN_SUCCESS = 'SET_LOGIN_SUCCESS';
const SET_LOGIN_ERROR = 'SET_LOGIN_ERROR';
export function login(email, password) {
return dispatch => {
dispatch(setLoginPending(true));
dispatch(setLoginSuccess(false));
dispatch(setLoginError(null));
callLoginApi(email, password, error => {
dispatch(setLoginPending(false));
if (!error) {
dispatch(setLoginSuccess(true));
} else {
dispatch(setLoginError(error));
}
});
}
}
function setLoginPending(isLoginPending) {
return {
type: SET_LOGIN_PENDING,
isLoginPending
};
}
function setLoginSuccess(isLoginSuccess) {
return {
type: SET_LOGIN_SUCCESS,
isLoginSuccess
};
}
function setLoginError(loginError) {
return {
type: SET_LOGIN_ERROR,
loginError
}
}
function callLoginApi(username, password, callback) {
Promise.resolve()
fetch('https://apollo.kevinmidboe.com/api/v1/user/login', {
method: 'POST',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({
username: username,
password: password,
})
})
.then(response => {
switch (response.status) {
case 200:
response.json()
.then((data) => {
if (data.success === true) {
let token = data.token;
setCookie('token', token, 10);
setCookie('logged_in', true, 10);
setCookie('loggedInUser', username, 10);
window.location.reload();
}
return callback(null);
})
case 401:
return callback(new Error(response.statusText));
}
})
.catch(error => {
return callback(new Error('Invalid username and password'));
});
}
export default function reducer(state = {
isLoginSuccess: false,
isLoginPending: false,
loginError: null
}, action) {
switch (action.type) {
case SET_LOGIN_PENDING:
return Object.assign({}, state, {
isLoginPending: action.isLoginPending
});
case SET_LOGIN_SUCCESS:
return Object.assign({}, state, {
isLoginSuccess: action.isLoginSuccess
});
case SET_LOGIN_ERROR:
return Object.assign({}, state, {
loginError: action.loginError
});
default:
return state;
}
}

View File

@@ -0,0 +1,7 @@
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import reducer from './reducer.jsx';
const store = createStore(reducer, {}, applyMiddleware(thunk, logger));
export default store;

View File

@@ -0,0 +1,16 @@
export default {
sidebar: {
float: 'left',
width: '18%',
minWidth: '250px',
fontFamily: '"Open Sans", sans-serif',
fontSize: '14px',
borderRight: '2px solid #f2f2f2',
},
selectedObjectPanel: {
width: '80%',
float: 'right',
fontFamily: '"Open Sans", sans-serif',
marginTop: '1em',
}
}

View File

@@ -0,0 +1,58 @@
export default {
wrapper: {
width: '100%',
},
stick: {
marginBottom: '1em',
},
title: {
fontSize: '2em',
},
image: {
width: '105px',
borderRadius: '4px',
},
info: {
paddingTop: '1em',
paddingBottom: '0.5em',
marginRight: '2em',
backgroundColor: 'white',
border: '1px solid #d0d0d0',
borderRadius: '2px',
display: 'flex',
},
type_icon: {
marginLeft: '-0.2em',
marginRight: '0.7em',
},
type_text: {
verticalAlign: 'super',
},
info_poster: {
marginLeft: '2em',
flex: '0 1 10%'
},
info_request: {
flex: '0 1 auto'
},
info_request_header: {
margin: '0',
marginBottom: '0.5em',
},
info_movie: {
maxWidth: '70%',
marginLeft: '1em',
flex: '0 1 auto',
},
info_movie_header: {
margin: '0',
marginBottom: '0.5em',
}
}

View File

@@ -0,0 +1,153 @@
export default {
header: {
textAlign: 'center',
},
body: {
backgroundColor: 'white',
},
parentElement: {
display: 'inline-block',
width: '100%',
border: '1px solid grey',
borderRadius: '2px',
padding: '4px',
margin: '4px',
marginLeft: '4px',
backgroundColor: 'white',
},
parentElement_hover: {
backgroundColor: '#f8f8f8',
pointer: 'hand',
},
parentElement_active: {
textDecoration: 'none',
},
parentElement_selected: {
display: 'inline-block',
width: '100%',
border: '1px solid grey',
borderRadius: '2px',
padding: '4px',
margin: '4px 0px 4px 4px',
marginLeft: '10px',
backgroundColor: 'white',
},
title: {
maxWidth: '65%',
display: 'inline-flex',
},
link: {
color: 'black',
textDecoration: 'none',
},
rightContainer: {
float: 'right',
},
searchSidebar: {
height: '4em',
},
searchInner: {
top: '0',
right: '0',
left: '0',
bottom: '0',
margin: 'auto',
width: '90%',
minWidth: '280px',
height: '30px',
border: '1px solid #d0d0d0',
borderRadius: '4px',
overflow: 'hidden'
},
searchTextField: {
display: 'inline-block',
width: '90%',
padding: '.3em',
verticalAlign: 'middle',
border: 'none',
background: '#fff',
fontSize: '14px',
marginTop: '-7px',
},
searchIcon: {
width: '15px',
height: '16px',
marginRight: '4px',
marginTop: '7px',
},
searchSVGIcon: {
fill: 'none',
stroke: '#9d9d9d',
strokeLinecap: 'round',
strokeLinejoin: 'round',
strokeMiterlimit: '10',
},
ulFilterSelectors: {
borderBottom: '2px solid #f1f1f1',
display: 'flex',
padding: '0',
margin: '0',
listStyle: 'none',
justifyContent: 'space-evenly',
},
aFilterSelectors: {
color: '#3eaaaf',
fontSize: '16px',
cursor: 'pointer',
},
spanFilterSelectors: {
content: '""',
bottom: '-2px',
display: 'block',
width: '100%',
height: '2px',
backgroundColor: '#3eaaaa',
},
ulCard: {
margin: '1em 0 0 0',
padding: '0',
listStyle: 'none',
borderBottom: '.46rem solid #f1f1f',
backgroundColor: '#f1f1f1',
overflow: 'scroll',
},
card: {
padding: '.1em .5em .8em 1.5em',
marginBottom: '.26rem',
height: 'auto',
cursor: 'pointer',
},
cardSelected: {
padding: '.1em .5em .8em 1.5em',
marginBottom: '.26rem',
height: 'auto',
cursor: 'pointer',
backgroundColor: '#f9f9f9',
},
titleCard: {
fontSize: '15px',
fontWeight: '400',
whiteSpace: 'no-wrap',
textDecoration: 'none',
},
pCard: {
margin: '0',
},
}

View File

@@ -0,0 +1,59 @@
export default {
table: {
width: '80%',
marginRight: 'auto',
marginLeft: 'auto',
},
tableHeader: {
},
col: {
cursor: 'pointer',
borderBottom: '1px solid #e0e0e0',
paddingBottom: '0.5em',
textAlign: 'left',
},
bodyCol: {
marginTop: '0.5em',
},
searchSidebar: {
height: '4em',
marginTop: '1em',
},
searchInner: {
top: '0',
right: '0',
left: '0',
bottom: '0',
margin: 'auto',
width: '50%',
minWidth: '280px',
height: '30px',
border: '1px solid #d0d0d0',
borderRadius: '4px',
overflow: 'hidden'
},
searchTextField: {
display: 'inline-block',
width: '95%',
padding: '.3em',
verticalAlign: 'middle',
border: 'none',
background: '#fff',
fontSize: '14px',
marginTop: '-7px',
},
searchIcon: {
width: '15px',
height: '16px',
marginRight: '4px',
marginTop: '7px',
},
searchSVGIcon: {
fill: 'none',
stroke: '#9d9d9d',
strokeLinecap: 'round',
strokeLinejoin: 'round',
strokeMiterlimit: '10',
},
}

View 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',
},
}

View File

@@ -0,0 +1,24 @@
export default {
bodyDiv: {
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
flexFlow: 'row wrap',
justifyContent: 'space-around',
},
wrappingDiv: {
},
requestPoster: {
height: '150px',
},
infoDiv: {
marginTop: 0,
marginLeft: '10px',
float: 'right',
},
}

View File

@@ -0,0 +1,62 @@
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%'
},
buttons: {
paddingTop: '20px',
},
summary: {
fontSize: '15px',
},
dividerRow: {
width: '100%'
},
itemDivider: {
width: '90%',
borderBottom: '1px solid grey',
margin: '2rem auto'
}
}

View File

@@ -0,0 +1,177 @@
export default {
body: {
fontFamily: "'Open Sans', sans-serif",
backgroundColor: '#f7f7f7',
margin: 0,
padding: 0,
minHeight: '100%',
},
backgroundLargeHeader: {
width: '100%',
minHeight: '180px',
backgroundColor: 'rgb(1, 28, 35)',
// backgroundImage: 'radial-gradient(circle, #004c67 0, #005771 120%)',
zIndex: 1,
marginBottom: '70px'
},
backgroundSmallHeader: {
width: '100%',
minHeight: '120px',
backgroundColor: '#011c23',
zIndex: 1,
marginBottom: '40px'
},
requestWrapper: {
maxWidth: '1200px',
margin: 'auto',
paddingTop: '10px',
backgroundColor: 'white',
position: 'relative',
zIndex: '10',
boxShadow: '0 1px 2px grey',
},
pageTitle: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
},
pageTitleLargeSpan: {
color: 'white',
fontSize: '3em',
marginTop: '4vh',
marginBottom: '6vh'
},
pageTitleSmallSpan: {
color: 'white',
fontSize: '2em',
marginTop: '3vh',
marginBottom: '3vh'
},
searchLargeContainer: {
height: '52px',
width: '77%',
paddingLeft: '23%',
backgroundColor: 'white',
boxShadow: 'grey 0px 1px 2px',
},
searchSmallContainer: {
},
searchIcon: {
position: 'absolute',
fontSize: '1.6em',
marginTop: '7px',
color: '#4f5b66',
display: 'block',
},
searchLargeBar: {
width: '50%',
height: '50px',
background: '#ffffff',
border: 'none',
fontSize: '12pt',
float: 'left',
color: '#63717f',
paddingLeft: '40px',
},
searchSmallBar: {
width: '100%',
height: '50px',
background: '#ffffff',
border: 'none',
fontSize: '11pt',
float: 'left',
color: '#63717f',
paddingLeft: '65px',
marginLeft: '-25px',
borderRadius: '5px',
},
// Dropdown for selecting tmdb lists
controls: {
textAlign: 'left',
paddingTop: '8px',
width: '33.3333%',
marginLeft: '0',
marginRight: '0',
},
withData: {
boxSizing: 'border-box',
marginBottom: '0',
display: 'block',
padding: '0',
verticalAlign: 'baseline',
font: 'inherit',
textAlign: 'left',
boxSizing: 'border-box',
},
sortOptions: {
border: '1px solid #000',
maxWidth: '100%',
overflow: 'hidden',
lineHeight: 'normal',
textAlign: 'left',
padding: '4px 12px',
paddingRight: '2rem',
backgroundImage: 'url("data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxOCAxOCI+CiAgPHRpdGxlPmFycm93LWRvd24tbWljcm88L3RpdGxlPgogIDxwb2x5bGluZSBwb2ludHM9IjE0IDQuNjcgOSAxMy4zMyA0IDQuNjciIHN0eWxlPSJmaWxsOiBub25lO3N0cm9rZTogIzAwMDtzdHJva2UtbWl0ZXJsaW1pdDogMTA7c3Ryb2tlLXdpZHRoOiAycHgiLz4KPC9zdmc+Cg==")',
backgroundSize: '18px 18px',
backgroundPosition: 'right 8px center',
backgroundRepeat: 'no-repeat',
width: 'auto',
display: 'inline-block',
outline: 'none',
boxSizing: 'border-box',
fontSize: '15px',
WebkitAppearance: 'none',
MozAppearance: 'none',
appearance: 'none',
},
searchFilterActive: {
color: '#00d17c',
fontSize: '1em',
marginLeft: '10px',
cursor: 'pointer'
},
searchFilterNotActive: {
color: 'white',
fontSize: '1em',
marginLeft: '10px',
cursor: 'pointer'
},
filter: {
color: 'white',
paddingLeft: '40px',
width: '60%',
},
resultLargeHeader: {
color: 'black',
fontSize: '1.6em',
width: '20%',
},
resultSmallHeader: {
paddingLeft: '12px',
color: 'black',
fontSize: '1.4em',
},
}

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<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">
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0, user-scalable=0">
<title>seasoned Shows</title>
</head>
<body style='margin: 0'>
<div id="root">
</div>
</body>
</html>

View File

@@ -0,0 +1,20 @@
/*
* @Author: KevinMidboe
* @Date: 2017-06-01 21:08:55
* @Last Modified by: KevinMidboe
* @Last Modified time: 2017-10-20 19:24:52
./client/index.js
which is the webpack entry file
*/
import React from 'react';
import { render } from 'react-dom';
import { HashRouter } from 'react-router-dom';
import Root from './Root.jsx';
render((
<HashRouter>
<Root />
</HashRouter>
), document.getElementById('root'));

View File

@@ -0,0 +1,44 @@
{
"name": "seasoned",
"version": "1.0.0",
"main": "index.js",
"repository": "https://github.com/KevinMidboe/seasonedShows",
"author": "Kevin Midboe",
"license": "MIT",
"scripts": {
"start": "webpack-dev-server --open --config webpack.dev.js",
"build": "NODE_ENV=production webpack --config webpack.prod.js",
"build_dev": "webpack --config webpack.dev.js"
},
"dependencies": {
"clean-webpack-plugin": "^0.1.17",
"css-loader": "^1.0.0",
"html-webpack-plugin": "^2.28.0",
"path": "^0.12.7",
"react": "^15.6.1",
"react-burger-menu": "^2.1.6",
"react-dom": "^15.5.4",
"react-infinite-scroller": "^1.0.15",
"react-interactive": "^0.8.1",
"react-notify-toast": "^0.3.2",
"react-redux": "^5.0.6",
"react-responsive": "^1.3.4",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"redux": "^3.7.2",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0",
"urijs": "^1.18.12",
"webfontloader": "^1.6.28",
"webpack": "^4.0.0",
"webpack-dev-server": "^3.1.11",
"webpack-merge": "^4.1.0"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1"
}
}

View File

@@ -0,0 +1,33 @@
/*
* @Author: KevinMidboe
* @Date: 2017-06-01 19:09:16
* @Last Modified by: KevinMidboe
* @Last Modified time: 2017-10-24 21:55:41
*/
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: './app/index.js',
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
template: './app/index.html',
})
],
module: {
loaders: [
{ test: /\.(js|jsx)$/, loader: 'babel-loader', exclude: /node_modules/ },
]
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};

View File

@@ -0,0 +1,17 @@
/*
* @Author: KevinMidboe
* @Date: 2017-06-01 19:09:16
* @Last Modified by: KevinMidboe
* @Last Modified time: 2017-10-24 22:12:52
*/
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
headers: {'Access-Control-Allow-Origin': '*'}
}
});;

View File

@@ -0,0 +1,28 @@
/*
* @Author: KevinMidboe
* @Date: 2017-06-01 19:09:16
* @Last Modified by: KevinMidboe
* @Last Modified time: 2017-10-24 22:26:29
*/
const merge = require('webpack-merge');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const common = require('./webpack.common.js');
var webpack = require('webpack')
module.exports = merge(common, {
plugins: [
new UglifyJSPlugin(),
new HtmlWebpackPlugin({
template: './app/index.html',
title: 'Caching'
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
}),
],
output: {
filename: '[name].[chunkhash].js',
}
});