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

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;