Moved old webpage, app & client into .archive folder
This commit is contained in:
92
.archive/client/app/components/admin/Admin.jsx
Normal file
92
.archive/client/app/components/admin/Admin.jsx
Normal 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;
|
||||
218
.archive/client/app/components/admin/AdminRequestInfo.jsx
Normal file
218
.archive/client/app/components/admin/AdminRequestInfo.jsx
Normal 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;
|
||||
66
.archive/client/app/components/admin/LoginForm/LoginForm.jsx
Normal file
66
.archive/client/app/components/admin/LoginForm/LoginForm.jsx
Normal 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);
|
||||
95
.archive/client/app/components/admin/PirateSearch.jsx
Normal file
95
.archive/client/app/components/admin/PirateSearch.jsx
Normal 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
|
||||
247
.archive/client/app/components/admin/Sidebar.jsx
Normal file
247
.archive/client/app/components/admin/Sidebar.jsx
Normal 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>
|
||||
{ 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;
|
||||
209
.archive/client/app/components/admin/TorrentTable.jsx
Normal file
209
.archive/client/app/components/admin/TorrentTable.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user