Merge pull request #323 from lbryio/react-upload
React/Redux - publish component
This commit is contained in:
commit
2f0df2df8f
82 changed files with 21151 additions and 1019 deletions
19
package.json
19
package.json
|
@ -9,7 +9,9 @@
|
|||
"start": "node speech.js",
|
||||
"lint": "eslint .",
|
||||
"fix": "eslint . --fix",
|
||||
"precommit": "eslint ."
|
||||
"precommit": "eslint .",
|
||||
"babel": "babel",
|
||||
"webpack": "webpack"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -41,16 +43,27 @@
|
|||
"nodemon": "^1.11.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": "^16.2.0",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-redux": "^5.0.6",
|
||||
"redux": "^3.7.2",
|
||||
"request": "^2.83.0",
|
||||
"request-promise": "^4.2.2",
|
||||
"sequelize": "^4.1.0",
|
||||
"sequelize-cli": "^3.0.0-3",
|
||||
"sleep": "^5.1.1",
|
||||
"universal-analytics": "^0.4.13",
|
||||
"whatwg-fetch": "^2.0.3",
|
||||
"winston": "^2.3.1",
|
||||
"winston-slack-webhook": "billbitt/winston-slack-webhook"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"chai": "^4.1.2",
|
||||
"chai-http": "^3.0.0",
|
||||
"eslint": "3.19.0",
|
||||
|
@ -61,6 +74,8 @@
|
|||
"eslint-plugin-react": "6.10.3",
|
||||
"eslint-plugin-standard": "3.0.1",
|
||||
"husky": "^0.13.4",
|
||||
"mocha": "^4.0.1"
|
||||
"mocha": "^4.0.1",
|
||||
"redux-devtools": "^3.4.1",
|
||||
"webpack": "^3.10.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ module.exports = new PassportLocalStrategy(
|
|||
.then(user => {
|
||||
if (!user) {
|
||||
// logger.debug('no user found');
|
||||
return done(null, false, {message: 'Incorrect username or password.'});
|
||||
return done(null, false, {message: 'Incorrect username or password'});
|
||||
}
|
||||
user.comparePassword(password, (passwordErr, isMatch) => {
|
||||
if (passwordErr) {
|
||||
|
@ -46,7 +46,7 @@ module.exports = new PassportLocalStrategy(
|
|||
}
|
||||
if (!isMatch) {
|
||||
// logger.debug('incorrect password');
|
||||
return done(null, false, {message: 'Incorrect username or password.'});
|
||||
return done(null, false, {message: 'Incorrect username or password'});
|
||||
}
|
||||
logger.debug('Password was a match, returning User');
|
||||
return returnUserAndChannelInfo(user)
|
||||
|
|
|
@ -483,32 +483,31 @@ table {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
#primary-dropzone-instructions, #dropbzone-dragover {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.position-absolute {
|
||||
#dropzone-text-holder {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#asset-preview-holder {
|
||||
position: relative;
|
||||
#dropzone-dragover, #dropzone-instructions {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#asset-preview {
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
width: calc(100% - 1rem);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dim {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
/* Assets */
|
||||
|
||||
.asset {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#show-body #asset-boilerpate {
|
||||
|
|
BIN
public/assets/img/loading.gif
Normal file
BIN
public/assets/img/loading.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 9 KiB |
|
@ -1,4 +0,0 @@
|
|||
function sendAuthRequest (channelName, password, url) {
|
||||
const params = `username=${channelName}&password=${password}`;
|
||||
return postRequest(url, params);
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
const EMAIL_FORMAT = 'ERROR_EMAIL_FORMAT';
|
|
@ -2,13 +2,11 @@
|
|||
function showChannelCreateInProgressDisplay () {
|
||||
const publishChannelForm = document.getElementById('publish-channel-form');
|
||||
const inProgress = document.getElementById('channel-publish-in-progress');
|
||||
const channelProgressBar = document.getElementById('create-channel-progress-bar');
|
||||
publishChannelForm.hidden = true;
|
||||
inProgress.hidden = false;
|
||||
createProgressBar(channelProgressBar, 12);
|
||||
}
|
||||
|
||||
// display the content that shows channle creation is done
|
||||
// display the content that shows channel creation is done
|
||||
function showChannelCreateDoneDisplay() {
|
||||
const inProgress = document.getElementById('channel-publish-in-progress');
|
||||
inProgress.hidden=true;
|
||||
|
@ -22,27 +20,38 @@ function showChannelCreationError(msg) {
|
|||
}
|
||||
|
||||
function publishNewChannel (event) {
|
||||
const userName = document.getElementById('new-channel-name').value;
|
||||
const username = document.getElementById('new-channel-name').value;
|
||||
const password = document.getElementById('new-channel-password').value;
|
||||
// prevent default so this script can handle submission
|
||||
event.preventDefault();
|
||||
// validate submission
|
||||
validationFunctions.validateNewChannelSubmission(userName, password)
|
||||
validationFunctions.validateNewChannelSubmission(username, password)
|
||||
.then(() => {
|
||||
showChannelCreateInProgressDisplay();
|
||||
return sendAuthRequest(userName, password, '/signup') // post the request
|
||||
// return sendAuthRequest(userName, password, '/signup') // post the request
|
||||
return fetch('/signup', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({username, password}),
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(function(response) {
|
||||
if (response.ok){
|
||||
return response.json();
|
||||
} else {
|
||||
throw response;
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
throw error;
|
||||
})
|
||||
})
|
||||
.then(result => {
|
||||
setUserCookies(result.channelName, result.channelClaimId, result.shortChannelId);
|
||||
.then(signupResult => {
|
||||
console.log('signup success:', signupResult);
|
||||
showChannelCreateDoneDisplay();
|
||||
// if user is on the home page, update the needed elements without reloading
|
||||
if (window.location.pathname === '/') {
|
||||
replaceChannelOptionInPublishChannelSelect(result.channelName);
|
||||
replaceChannelOptionInNavBarChannelSelect(result.channelName);
|
||||
// if user is not on home page, redirect to home page
|
||||
} else {
|
||||
window.location = '/';
|
||||
}
|
||||
window.location = '/';
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.name === 'ChannelNameError' || error.name === 'ChannelPasswordError'){
|
||||
|
@ -50,7 +59,7 @@ function publishNewChannel (event) {
|
|||
validationFunctions.showError(channelNameErrorDisplayElement, error.message);
|
||||
} else {
|
||||
console.log('signup failure:', error);
|
||||
showChannelCreationError('Unfortunately, we encountered an error while creating your channel. Please let us know in slack!');
|
||||
showChannelCreationError('Unfortunately, we encountered an error while creating your channel. Please let us know in slack!', error);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
|
||||
function drop_handler(event) {
|
||||
event.preventDefault();
|
||||
// if dropped items aren't files, reject them
|
||||
var dt = event.dataTransfer;
|
||||
if (dt.items) {
|
||||
if (dt.items[0].kind == 'file') {
|
||||
var droppedFile = dt.items[0].getAsFile();
|
||||
publishFileFunctions.previewAndStageFile(droppedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dragover_handler(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function dragend_handler(event) {
|
||||
var dt = event.dataTransfer;
|
||||
if (dt.items) {
|
||||
for (var i = 0; i < dt.items.length; i++) {
|
||||
dt.items.remove(i);
|
||||
}
|
||||
} else {
|
||||
event.dataTransfer.clearData();
|
||||
}
|
||||
}
|
||||
|
||||
function dragenter_handler(event) {
|
||||
var thisDropzone = document.getElementById(event.target.id);
|
||||
thisDropzone.setAttribute('class', 'dropzone dropzone--drag-over row row--margined row--padded row--tall flex-container--column flex-container--center-center');
|
||||
thisDropzone.firstElementChild.setAttribute('class', 'hidden');
|
||||
thisDropzone.lastElementChild.setAttribute('class', '');
|
||||
|
||||
}
|
||||
|
||||
function dragexit_handler(event) {
|
||||
var thisDropzone = document.getElementById(event.target.id);
|
||||
thisDropzone.setAttribute('class', 'dropzone row row--tall row--margined row--padded flex-container--column flex-container--center-center');
|
||||
thisDropzone.firstElementChild.setAttribute('class', '');
|
||||
thisDropzone.lastElementChild.setAttribute('class', 'hidden');
|
||||
}
|
||||
|
||||
function preview_onmouseenter_handler () {
|
||||
document.getElementById('asset-preview-dropzone-instructions').setAttribute('class', 'flex-container--column flex-container--center-center position-absolute');
|
||||
document.getElementById('asset-preview').style.opacity = 0.2;
|
||||
}
|
||||
|
||||
function preview_onmouseleave_handler () {
|
||||
document.getElementById('asset-preview-dropzone-instructions').setAttribute('class', 'hidden');
|
||||
document.getElementById('asset-preview').style.opacity = 1;
|
||||
}
|
||||
|
|
@ -1,160 +1,3 @@
|
|||
function getRequest (url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhttp = new XMLHttpRequest();
|
||||
xhttp.open('GET', url, true);
|
||||
xhttp.responseType = 'json';
|
||||
xhttp.onreadystatechange = () => {
|
||||
if (xhttp.readyState == 4 ) {
|
||||
if ( xhttp.status == 200) {
|
||||
resolve(xhttp.response);
|
||||
} else if (xhttp.status == 401) {
|
||||
reject('Wrong channel name or password');
|
||||
} else {
|
||||
reject('request failed with status:' + xhttp.status);
|
||||
};
|
||||
}
|
||||
};
|
||||
xhttp.send();
|
||||
})
|
||||
}
|
||||
|
||||
function postRequest (url, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhttp = new XMLHttpRequest();
|
||||
xhttp.open('POST', url, true);
|
||||
xhttp.responseType = 'json';
|
||||
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhttp.onreadystatechange = () => {
|
||||
if (xhttp.readyState == 4 ) {
|
||||
if ( xhttp.status == 200) {
|
||||
resolve(xhttp.response);
|
||||
} else if (xhttp.status == 401) {
|
||||
reject( new AuthenticationError('Wrong channel name or password'));
|
||||
} else {
|
||||
reject('request failed with status:' + xhttp.status);
|
||||
};
|
||||
}
|
||||
};
|
||||
xhttp.send(params);
|
||||
})
|
||||
}
|
||||
|
||||
function toggleSection(event){
|
||||
event.preventDefault();
|
||||
|
||||
var dataSet = event.target.dataset;
|
||||
var status = dataSet.open;
|
||||
var masterElement = document.getElementById(event.target.id||event.srcElement.id);
|
||||
var slaveElement = document.getElementById(dataSet.slaveelementid);
|
||||
var closedLabel = dataSet.closedlabel;
|
||||
var openLabel = dataSet.openlabel;
|
||||
|
||||
if (status === "false") {
|
||||
slaveElement.hidden = false;
|
||||
masterElement.innerText = openLabel;
|
||||
masterElement.dataset.open = "true";
|
||||
} else {
|
||||
slaveElement.hidden = true;
|
||||
masterElement.innerText = closedLabel;
|
||||
masterElement.dataset.open = "false";
|
||||
}
|
||||
}
|
||||
|
||||
function createProgressBar(element, size){
|
||||
var x = 0;
|
||||
var adder = 1;
|
||||
// create the bar holder & place it
|
||||
var barHolder = document.createElement('p');
|
||||
for (var i = 0; i < size; i++) {
|
||||
const bar = document.createElement('span');
|
||||
bar.innerText = '| ';
|
||||
bar.setAttribute('class', 'progress-bar progress-bar--inactive');
|
||||
barHolder.appendChild(bar);
|
||||
}
|
||||
element.appendChild(barHolder);
|
||||
// get the bars
|
||||
const bars = document.getElementsByClassName('progress-bar');
|
||||
// function to update the bars' classes
|
||||
function updateOneBar(){
|
||||
// update the appropriate bar
|
||||
if (x > -1 && x < size){
|
||||
if (adder === 1){
|
||||
bars[x].setAttribute('class', 'progress-bar progress-bar--active');
|
||||
} else {
|
||||
bars[x].setAttribute('class', 'progress-bar progress-bar--inactive');
|
||||
}
|
||||
}
|
||||
// set x
|
||||
if (x === size){
|
||||
adder = -1;
|
||||
} else if ( x === -1){
|
||||
adder = 1;
|
||||
}
|
||||
// update the adder
|
||||
x += adder;
|
||||
|
||||
};
|
||||
// start updater
|
||||
setInterval(updateOneBar, 300);
|
||||
}
|
||||
|
||||
function setCookie(key, value) {
|
||||
document.cookie = `${key}=${value}`;
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
const name = cname + "=";
|
||||
const decodedCookie = decodeURIComponent(document.cookie);
|
||||
const ca = decodedCookie.split(';');
|
||||
for(let i = 0; i <ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function checkCookie() {
|
||||
const channelName = getCookie("channel_name");
|
||||
if (channelName != "") {
|
||||
console.log(`cookie found for ${channelName}`);
|
||||
} else {
|
||||
console.log('no channel_name cookie found');
|
||||
}
|
||||
}
|
||||
|
||||
function clearCookie(name) {
|
||||
document.cookie = `${name}=; expires=Thu, 01-Jan-1970 00:00:01 GMT;`;
|
||||
}
|
||||
|
||||
function setUserCookies(channelName, channelClaimId, shortChannelId) {
|
||||
setCookie('channel_name', channelName)
|
||||
setCookie('channel_claim_id', channelClaimId);
|
||||
setCookie('short_channel_id', shortChannelId);
|
||||
}
|
||||
|
||||
function clearUserCookies() {
|
||||
clearCookie('channel_name')
|
||||
clearCookie('channel_claim_id');
|
||||
clearCookie('short_channel_id');
|
||||
}
|
||||
|
||||
function copyToClipboard(event){
|
||||
var elementToCopy = event.target.dataset.elementtocopy;
|
||||
var element = document.getElementById(elementToCopy);
|
||||
var errorElement = 'input-error-copy-text' + elementToCopy;
|
||||
element.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
validationFunctions.showError(errorElement, 'Oops, unable to copy');
|
||||
}
|
||||
}
|
||||
|
||||
// Create new error objects, that prototypically inherit from the Error constructor
|
||||
function FileError(message) {
|
||||
this.name = 'FileError';
|
||||
|
|
|
@ -1,67 +1,41 @@
|
|||
function replaceChannelOptionInPublishChannelSelect(loggedInChannel) {
|
||||
// remove the old channel option
|
||||
const oldChannel = document.getElementById('publish-channel-select-channel-option')
|
||||
if (oldChannel){
|
||||
oldChannel.parentNode.removeChild(oldChannel);
|
||||
}
|
||||
// create new channel option
|
||||
const newChannelOption = document.createElement('option');
|
||||
newChannelOption.setAttribute('value', loggedInChannel);
|
||||
newChannelOption.setAttribute('id', 'publish-channel-select-channel-option');
|
||||
newChannelOption.setAttribute('selected', '');
|
||||
newChannelOption.innerText = loggedInChannel;
|
||||
// add the new option
|
||||
const channelSelect = document.getElementById('channel-name-select');
|
||||
channelSelect.insertBefore(newChannelOption, channelSelect.firstChild);
|
||||
// carry out channel selection
|
||||
toggleSelectedChannel(loggedInChannel);
|
||||
}
|
||||
|
||||
function replaceChannelOptionInNavBarChannelSelect (loggedInChannel) {
|
||||
// remove the old channel option
|
||||
const oldChannel = document.getElementById('nav-bar-channel-select-channel-option');
|
||||
if (oldChannel){
|
||||
oldChannel.parentNode.removeChild(oldChannel);
|
||||
}
|
||||
// create new channel option & select it
|
||||
const newChannelOption = document.createElement('option');
|
||||
newChannelOption.setAttribute('value', loggedInChannel);
|
||||
newChannelOption.setAttribute('id', 'nav-bar-channel-select-channel-option');
|
||||
newChannelOption.setAttribute('selected', '');
|
||||
newChannelOption.innerText = loggedInChannel;
|
||||
// add the new option
|
||||
const channelSelect = document.getElementById('nav-bar-channel-select');
|
||||
channelSelect.style.display = 'inline-block';
|
||||
channelSelect.insertBefore(newChannelOption, channelSelect.firstChild);
|
||||
// hide login
|
||||
const navBarLoginLink = document.getElementById('nav-bar-login-link');
|
||||
navBarLoginLink.style.display = 'none';
|
||||
}
|
||||
|
||||
function loginToChannel (event) {
|
||||
const userName = document.getElementById('channel-login-name-input').value;
|
||||
const username = document.getElementById('channel-login-name-input').value;
|
||||
const password = document.getElementById('channel-login-password-input').value;
|
||||
// prevent default
|
||||
event.preventDefault()
|
||||
validationFunctions.validateNewChannelLogin(userName, password)
|
||||
validationFunctions.validateNewChannelLogin(username, password)
|
||||
.then(() => {
|
||||
return sendAuthRequest(userName, password, '/login')
|
||||
return fetch('/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({username, password}),
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(function(response) {
|
||||
console.log(response);
|
||||
if (response.ok){
|
||||
return response.json();
|
||||
} else {
|
||||
throw response;
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
throw error;
|
||||
})
|
||||
})
|
||||
.then(result => {
|
||||
setUserCookies(result.channelName, result.channelClaimId, result.shortChannelId);
|
||||
// if user is on the home page, update the needed elements without reloading
|
||||
if (window.location.pathname === '/') {
|
||||
replaceChannelOptionInPublishChannelSelect(result.channelName);
|
||||
replaceChannelOptionInNavBarChannelSelect(result.channelName);
|
||||
// if user is not on home page, redirect to home page
|
||||
} else {
|
||||
.then(function({success, message}) {
|
||||
if (success) {
|
||||
window.location = '/';
|
||||
} else {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
const loginErrorDisplayElement = document.getElementById('login-error-display-element');
|
||||
if (error.name){
|
||||
if (error.message){
|
||||
validationFunctions.showError(loginErrorDisplayElement, error.message);
|
||||
} else {
|
||||
validationFunctions.showError(loginErrorDisplayElement, 'There was an error logging into your channel');
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
function toggleNavBarSelection (value) {
|
||||
const selectedOption = value;
|
||||
if (selectedOption === 'LOGOUT') {
|
||||
// remove session cookies
|
||||
clearUserCookies();
|
||||
// send logout request to server
|
||||
window.location.href = '/logout';
|
||||
} else if (selectedOption === 'VIEW') {
|
||||
// get channel info
|
||||
const channelName = getCookie('channel_name');
|
||||
const channelClaimId = getCookie('channel_claim_id');
|
||||
// redirect to channel page
|
||||
window.location.href = `/${channelName}:${channelClaimId}`;
|
||||
}
|
||||
}
|
|
@ -1,234 +0,0 @@
|
|||
var stagedFiles = null;
|
||||
|
||||
const publishFileFunctions = {
|
||||
triggerFileChooser: function (fileInputId) {
|
||||
document.getElementById(fileInputId).click();
|
||||
},
|
||||
cancelPublish: function () {
|
||||
window.location.href = '/';
|
||||
},
|
||||
previewAndStageFile: function (selectedFile) {
|
||||
const fileSelectionInputError = document.getElementById('input-error-file-selection');
|
||||
// When a file is selected for publish, validate that file and
|
||||
// stage it so it will be ready when the publish button is clicked
|
||||
try {
|
||||
validationFunctions.validateFile(selectedFile); // validate the file's name, type, and size
|
||||
} catch (error) {
|
||||
validationFunctions.showError(fileSelectionInputError, error.message);
|
||||
return;
|
||||
}
|
||||
// set image preview, if an image was provided
|
||||
this.setImagePreview(selectedFile);
|
||||
// hide the primary drop zone
|
||||
this.hidePrimaryDropzone();
|
||||
// set the name input value to the image name if none is set yet
|
||||
this.updateClaimNameInputWithFileName(selectedFile);
|
||||
// store the selected file for upload
|
||||
stagedFiles = [selectedFile];
|
||||
},
|
||||
hidePrimaryDropzone: function () {
|
||||
const primaryDropzone = document.getElementById('primary-dropzone');
|
||||
const publishForm = document.getElementById('publish-form');
|
||||
primaryDropzone.setAttribute('class', 'hidden');
|
||||
publishForm.setAttribute('class', 'row')
|
||||
},
|
||||
updateClaimNameInputWithFileName: function (selectedFile) {
|
||||
const nameInput = document.getElementById('claim-name-input');
|
||||
if (nameInput.value === "") {
|
||||
var filename = selectedFile.name.substring(0, selectedFile.name.indexOf('.'))
|
||||
nameInput.value = validationFunctions.cleanseClaimName(filename);
|
||||
validationFunctions.checkClaimName(nameInput.value);
|
||||
}
|
||||
},
|
||||
setImagePreview: function (selectedFile) {
|
||||
const assetPreview = document.getElementById('asset-preview-target');
|
||||
const thumbnailInput = document.getElementById('claim-thumbnail-input');
|
||||
const thumbnailInputTool = document.getElementById('publish-thumbnail');
|
||||
if (selectedFile.type !== 'video/mp4') {
|
||||
const previewReader = new FileReader();
|
||||
if (selectedFile.type === 'image/gif') {
|
||||
assetPreview.innerHTML = `<p>loading preview...</p>`
|
||||
}
|
||||
previewReader.readAsDataURL(selectedFile);
|
||||
previewReader.onloadend = function () {
|
||||
assetPreview.innerHTML = '<img id="asset-preview" src="' + previewReader.result + '" alt="image preview"/>';
|
||||
};
|
||||
// clear & hide the thumbnail selection input
|
||||
thumbnailInput.value = '';
|
||||
thumbnailInputTool.hidden = true;
|
||||
} else {
|
||||
assetPreview.innerHTML = `<img id="asset-preview" src="/assets/img/video_thumb_default.png"/>`;
|
||||
// clear & show the thumbnail selection input
|
||||
thumbnailInput.value = '';
|
||||
thumbnailInputTool.hidden = false;
|
||||
}
|
||||
},
|
||||
returnNullOrChannel: function () {
|
||||
const channelRadio = document.getElementById('channel-radio');
|
||||
if (channelRadio.checked) {
|
||||
const channelInput = document.getElementById('channel-name-select');
|
||||
return channelInput.value.trim();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
createMetadata: function() {
|
||||
const nameInput = document.getElementById('claim-name-input');
|
||||
const titleInput = document.getElementById('publish-title');
|
||||
const descriptionInput = document.getElementById('publish-description');
|
||||
const licenseInput = document.getElementById('publish-license');
|
||||
const nsfwInput = document.getElementById('publish-nsfw');
|
||||
const thumbnailInput = document.getElementById('claim-thumbnail-input');
|
||||
const channelName = this.returnNullOrChannel();
|
||||
let metadata = {
|
||||
name: nameInput.value.trim(),
|
||||
title: titleInput.value.trim(),
|
||||
description: descriptionInput.value.trim(),
|
||||
license: licenseInput.value.trim(),
|
||||
nsfw: nsfwInput.checked,
|
||||
type: stagedFiles[0].type,
|
||||
thumbnail: thumbnailInput.value.trim(),
|
||||
};
|
||||
if (channelName) {
|
||||
metadata['channelName'] = channelName;
|
||||
}
|
||||
return metadata;
|
||||
},
|
||||
appendDataToFormData: function (file, metadata) {
|
||||
var fd = new FormData();
|
||||
fd.append('file', file)
|
||||
for (var key in metadata) {
|
||||
if (metadata.hasOwnProperty(key)) {
|
||||
console.log(key, metadata[key]);
|
||||
fd.append(key, metadata[key]);
|
||||
}
|
||||
}
|
||||
return fd;
|
||||
},
|
||||
publishFile: function (file, metadata) {
|
||||
var uri = "/api/claim-publish";
|
||||
var xhr = new XMLHttpRequest();
|
||||
var fd = this.appendDataToFormData(file, metadata);
|
||||
var that = this;
|
||||
xhr.upload.addEventListener("loadstart", function(e) {
|
||||
that.showUploadStartedMessage();
|
||||
})
|
||||
xhr.upload.addEventListener("progress", function(e) {
|
||||
if (e.lengthComputable) {
|
||||
var percentage = Math.round((e.loaded * 100) / e.total);
|
||||
console.log('progress:', percentage);
|
||||
that.showUploadProgressMessage(percentage);
|
||||
}
|
||||
}, false);
|
||||
xhr.upload.addEventListener("load", function(e){
|
||||
console.log('loaded 100%');
|
||||
that.showFilePublishUpdate("Your file has been loaded, and is now being published to the blockchain. Sit tight...")
|
||||
}, false);
|
||||
xhr.open("POST", uri, true);
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
console.log('publish response:', xhr.response)
|
||||
if (xhr.status == 200) {
|
||||
console.log('publish complete!');
|
||||
that.showFilePublishComplete(JSON.parse(xhr.response).message);
|
||||
} else if (xhr.status == 502){
|
||||
that.showFilePublishFailure('Spee.ch was not able to get a response from the LBRY network.');
|
||||
} else {
|
||||
that.showFilePublishFailure(JSON.parse(xhr.response).message);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Initiate a multipart/form-data upload
|
||||
xhr.send(fd);
|
||||
},
|
||||
// Validate the publish submission and then trigger upload
|
||||
publishStagedFile: function (event) {
|
||||
event.preventDefault(); // prevent default so this script can handle submission
|
||||
const metadata = this.createMetadata();
|
||||
const that = this;
|
||||
const fileSelectionInputError = document.getElementById('input-error-file-selection');
|
||||
const claimNameError = document.getElementById('input-error-claim-name');
|
||||
const channelSelectError = document.getElementById('input-error-channel-select');
|
||||
const publishSubmitError = document.getElementById('input-error-publish-submit');
|
||||
// validate, submit, and handle response
|
||||
validationFunctions.validateFilePublishSubmission(stagedFiles, metadata)
|
||||
.then( function() {
|
||||
that.publishFile(stagedFiles[0], metadata);
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.name === 'FileError') {
|
||||
validationFunctions.showError(fileSelectionInputError, error.message);
|
||||
} else if (error.name === 'NameError') {
|
||||
validationFunctions.showError(claimNameError, error.message);
|
||||
} else if (error.name === 'ChannelNameError'){
|
||||
console.log(error);
|
||||
validationFunctions.showError(channelSelectError, error.message);
|
||||
} else {
|
||||
validationFunctions.showError(publishSubmitError, error.message);
|
||||
}
|
||||
return;
|
||||
})
|
||||
},
|
||||
showUploadStartedMessage: function (){
|
||||
console.log('starting upload');
|
||||
// hide the publish tool
|
||||
this.hidePublishTools();
|
||||
// show the progress status and animation
|
||||
this.showPublishStatus();
|
||||
this.showPublishProgressBar();
|
||||
},
|
||||
showUploadProgressMessage: function (percentage){
|
||||
this.updatePublishStatus('<p>File is loading to server</p>');
|
||||
this.updateUploadPercent('<p class="blue">' + percentage + '% </p>');
|
||||
},
|
||||
showFilePublishUpdate: function (msg) {
|
||||
this.updatePublishStatus('<p>' + msg + '</p>');
|
||||
this.updateUploadPercent('<p>Curious what magic is happening here? <a class="link--primary" target="blank" href="https://lbry.io/faq/what-is-lbry">Learn more.</a></p>');
|
||||
},
|
||||
showFilePublishFailure: function (msg){
|
||||
this.updatePublishStatus('<p>Something went wrong...</p><p><strong>' + msg + '</strong></p><p>For help, post the above error text in the #speech channel on the <a class="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">lbry discord</a>');
|
||||
this.hidePublishProgressBar();
|
||||
this.hideUploadPercent();
|
||||
},
|
||||
showFilePublishComplete: function (msg) {
|
||||
console.log('Publish complete!');
|
||||
const showUrl = msg.lbryTx.claim_id + "/" + msg.name;
|
||||
// update status
|
||||
this.updatePublishStatus('<p>Your publish is complete! You are being redirected to it now.</p>');
|
||||
this.updateUploadPercent('<p><a class="link--primary" target="_blank" href="' + showUrl + '">If you do not get redirected, click here.</a></p>')
|
||||
// redirect the user
|
||||
window.location.href = showUrl;
|
||||
},
|
||||
hidePublishTools: function () {
|
||||
const publishFormWrapper = document.getElementById('publish-form');
|
||||
publishFormWrapper.setAttribute('class', 'hidden');
|
||||
},
|
||||
// publish status functions
|
||||
showPublishStatus: function () {
|
||||
const publishStatus = document.getElementById('publish-status');
|
||||
publishStatus.setAttribute('class', 'row row--tall flex-container--column flex-container--center-center');
|
||||
},
|
||||
updatePublishStatus: function (msg){
|
||||
const publishUpdate = document.getElementById('publish-update');
|
||||
publishUpdate.innerHTML = msg;
|
||||
},
|
||||
// progress bar functions
|
||||
showPublishProgressBar: function (){
|
||||
const publishProgressBar = document.getElementById('publish-progress-bar');
|
||||
createProgressBar(publishProgressBar, 12);
|
||||
},
|
||||
hidePublishProgressBar: function (){
|
||||
const publishProgressBar = document.getElementById('publish-progress-bar');
|
||||
publishProgressBar.hidden = true;
|
||||
},
|
||||
// upload percent functions
|
||||
updateUploadPercent: function (msg){
|
||||
const uploadPercent = document.getElementById('upload-percent');
|
||||
uploadPercent.innerHTML = msg;
|
||||
},
|
||||
hideUploadPercent: function (){
|
||||
const uploadPercent = document.getElementById('upload-percent');
|
||||
uploadPercent.hidden = true;
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -1,53 +1,5 @@
|
|||
// validation function which checks the proposed file's type, size, and name
|
||||
const validationFunctions = {
|
||||
validateFile: function (file) {
|
||||
if (!file) {
|
||||
console.log('no file found');
|
||||
throw new Error('no file provided');
|
||||
}
|
||||
if (/'/.test(file.name)) {
|
||||
console.log('file name had apostrophe in it');
|
||||
throw new Error('apostrophes are not allowed in the file name');
|
||||
}
|
||||
// validate size and type
|
||||
switch (file.type) {
|
||||
case 'image/jpeg':
|
||||
case 'image/jpg':
|
||||
case 'image/png':
|
||||
if (file.size > 10000000) {
|
||||
console.log('file was too big');
|
||||
throw new Error('Sorry, images are limited to 10 megabytes.');
|
||||
}
|
||||
break;
|
||||
case 'image/gif':
|
||||
if (file.size > 50000000) {
|
||||
console.log('file was too big');
|
||||
throw new Error('Sorry, .gifs are limited to 50 megabytes.');
|
||||
}
|
||||
break;
|
||||
case 'video/mp4':
|
||||
if (file.size > 50000000) {
|
||||
console.log('file was too big');
|
||||
throw new Error('Sorry, videos are limited to 50 megabytes.');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log('file type is not supported');
|
||||
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.')
|
||||
}
|
||||
},
|
||||
// validation function that checks to make sure the claim name is valid
|
||||
validateClaimName: function (name) {
|
||||
// ensure a name was entered
|
||||
if (name.length < 1) {
|
||||
throw new NameError("You must enter a name for your url");
|
||||
}
|
||||
// validate the characters in the 'name' field
|
||||
const invalidCharacters = /[^A-Za-z0-9,-]/g.exec(name);
|
||||
if (invalidCharacters) {
|
||||
throw new NameError('"' + invalidCharacters + '" characters are not allowed');
|
||||
}
|
||||
},
|
||||
validateChannelName: function (name) {
|
||||
name = name.substring(name.indexOf('@') + 1);
|
||||
// ensure a name was entered
|
||||
|
@ -65,15 +17,21 @@ const validationFunctions = {
|
|||
throw new ChannelPasswordError("You must enter a password for you channel");
|
||||
}
|
||||
},
|
||||
cleanseClaimName: function (name) {
|
||||
name = name.replace(/\s+/g, '-'); // replace spaces with dashes
|
||||
name = name.replace(/[^A-Za-z0-9-]/g, ''); // remove all characters that are not A-Z, a-z, 0-9, or '-'
|
||||
return name;
|
||||
},
|
||||
// validation functions to check claim & channel name eligibility as the inputs change
|
||||
isChannelNameAvailable: function (name) {
|
||||
return this.isNameAvailable(name, '/api/channel-is-available/');
|
||||
},
|
||||
isNameAvailable: function (name, apiUrl) {
|
||||
console.log('isNameAvailable?', name);
|
||||
const url = apiUrl + name;
|
||||
return getRequest(url)
|
||||
return fetch(url)
|
||||
.then(function (response) {
|
||||
return response.json();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('isNameAvailable error', error);
|
||||
throw error;
|
||||
})
|
||||
},
|
||||
showError: function (errorDisplay, errorMsg) {
|
||||
errorDisplay.hidden = false;
|
||||
|
@ -91,90 +49,35 @@ const validationFunctions = {
|
|||
successElement.hidden = true;
|
||||
successElement.innerHTML = "";
|
||||
},
|
||||
checkAvailability: function (name, successDisplayElement, errorDisplayElement, validateName, errorMessage, apiUrl) {
|
||||
checkChannelName: function (name) {
|
||||
var successDisplayElement = document.getElementById('input-success-channel-name');
|
||||
var errorDisplayElement = document.getElementById('input-error-channel-name');
|
||||
var channelName = `@${name}`;
|
||||
var that = this;
|
||||
try {
|
||||
// check to make sure the characters are valid
|
||||
validateName(name);
|
||||
that.validateChannelName(channelName);
|
||||
// check to make sure it is available
|
||||
that.isNameAvailable(name, apiUrl)
|
||||
.then(function (result) {
|
||||
if (result === true) {
|
||||
that.hideError(errorDisplayElement);
|
||||
that.showSuccess(successDisplayElement)
|
||||
} else {
|
||||
that.hideSuccess(successDisplayElement);
|
||||
that.showError(errorDisplayElement, errorMessage);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
that.hideSuccess(successDisplayElement);
|
||||
that.showError(errorDisplayElement, error.message);
|
||||
});
|
||||
that.isChannelNameAvailable(channelName)
|
||||
.then(function(isAvailable){
|
||||
console.log('isChannelNameAvailable:', isAvailable);
|
||||
if (isAvailable) {
|
||||
that.hideError(errorDisplayElement);
|
||||
that.showSuccess(successDisplayElement)
|
||||
} else {
|
||||
that.hideSuccess(successDisplayElement);
|
||||
that.showError(errorDisplayElement, 'Sorry, that name is already taken');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
that.hideSuccess(successDisplayElement);
|
||||
that.showError(errorDisplayElement, error.message);
|
||||
});
|
||||
} catch (error) {
|
||||
that.hideSuccess(successDisplayElement);
|
||||
that.showError(errorDisplayElement, error.message);
|
||||
}
|
||||
},
|
||||
checkClaimName: function (name) {
|
||||
const successDisplayElement = document.getElementById('input-success-claim-name');
|
||||
const errorDisplayElement = document.getElementById('input-error-claim-name');
|
||||
this.checkAvailability(name, successDisplayElement, errorDisplayElement, this.validateClaimName, 'Sorry, that ending is already taken', '/api/claim-is-available/');
|
||||
},
|
||||
checkChannelName: function (name) {
|
||||
const successDisplayElement = document.getElementById('input-success-channel-name');
|
||||
const errorDisplayElement = document.getElementById('input-error-channel-name');
|
||||
name = `@${name}`;
|
||||
this.checkAvailability(name, successDisplayElement, errorDisplayElement, this.validateChannelName, 'Sorry, that name is already taken', '/api/channel-is-available/');
|
||||
},
|
||||
// validation function which checks all aspects of the publish submission
|
||||
validateFilePublishSubmission: function (stagedFiles, metadata) {
|
||||
const channelName = metadata.channelName;
|
||||
const claimName = metadata.name;
|
||||
var that = this;
|
||||
return new Promise(function (resolve, reject) {
|
||||
// 1. make sure 1 file was staged
|
||||
if (!stagedFiles) {
|
||||
reject(new FileError("Please select a file"));
|
||||
return;
|
||||
} else if (stagedFiles.length > 1) {
|
||||
reject(new FileError("Only one file is allowed at a time"));
|
||||
return;
|
||||
}
|
||||
// 2. validate the file's name, type, and size
|
||||
try {
|
||||
that.validateFile(stagedFiles[0]);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
// 3. validate that a channel was chosen
|
||||
if (channelName === 'new' || channelName === 'login') {
|
||||
reject(new ChannelNameError("Please log in to a channel"));
|
||||
return;
|
||||
}
|
||||
;
|
||||
// 4. validate the claim name
|
||||
try {
|
||||
that.validateClaimName(claimName);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
// if all validation passes, check availability of the name (note: do we need to re-validate channel name vs. credentials as well?)
|
||||
return that.isNameAvailable(claimName, '/api/claim-is-available/')
|
||||
.then(result => {
|
||||
if (result) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new NameError('Sorry, that ending is already taken'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
// validation function which checks all aspects of a new channel submission
|
||||
validateNewChannelSubmission: function (userName, password) {
|
||||
const channelName = `@${userName}`;
|
||||
|
@ -193,16 +96,15 @@ const validationFunctions = {
|
|||
return reject(error);
|
||||
}
|
||||
// 3. if all validation passes, check availability of the name
|
||||
that.isNameAvailable(channelName, '/api/channel-is-available/') // validate the availability
|
||||
.then(function(result) {
|
||||
if (result) {
|
||||
that.isChannelNameAvailable(channelName)
|
||||
.then(function(isAvailable) {
|
||||
if (isAvailable) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new ChannelNameError('Sorry, that name is already taken'));
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.log('error evaluating channel name availability', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
@ -227,4 +129,4 @@ const validationFunctions = {
|
|||
resolve();
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
19012
public/bundle/bundle.js
Normal file
19012
public/bundle/bundle.js
Normal file
File diff suppressed because it is too large
Load diff
12
react/actions/channel.js
Normal file
12
react/actions/channel.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import * as actions from 'constants/channel_action_types';
|
||||
|
||||
// export action creators
|
||||
|
||||
export function updateLoggedInChannel (name, shortId, longId) {
|
||||
return {
|
||||
type: actions.CHANNEL_UPDATE,
|
||||
name,
|
||||
shortId,
|
||||
longId,
|
||||
};
|
||||
};
|
67
react/actions/publish.js
Normal file
67
react/actions/publish.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import * as actions from 'constants/publish_action_types';
|
||||
|
||||
// export action creators
|
||||
export function selectFile (file) {
|
||||
return {
|
||||
type: actions.FILE_SELECTED,
|
||||
file: file,
|
||||
};
|
||||
};
|
||||
|
||||
export function clearFile () {
|
||||
return {
|
||||
type: actions.FILE_CLEAR,
|
||||
};
|
||||
};
|
||||
|
||||
export function updateMetadata (name, value) {
|
||||
return {
|
||||
type: actions.METADATA_UPDATE,
|
||||
name,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
export function updateClaim (value) {
|
||||
return {
|
||||
type: actions.CLAIM_UPDATE,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
export function setPublishInChannel (channel) {
|
||||
return {
|
||||
type: actions.SET_PUBLISH_IN_CHANNEL,
|
||||
channel,
|
||||
};
|
||||
};
|
||||
|
||||
export function updatePublishStatus (status, message) {
|
||||
return {
|
||||
type: actions.PUBLISH_STATUS_UPDATE,
|
||||
status,
|
||||
message,
|
||||
};
|
||||
};
|
||||
|
||||
export function updateError (name, value) {
|
||||
return {
|
||||
type: actions.ERROR_UPDATE,
|
||||
name,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
export function updateSelectedChannel (value) {
|
||||
return {
|
||||
type: actions.SELECTED_CHANNEL_UPDATE,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
export function toggleMetadataInputs (value) {
|
||||
return {
|
||||
type: actions.TOGGLE_METADATA_INPUTS,
|
||||
value,
|
||||
};
|
||||
};
|
26
react/app.js
Normal file
26
react/app.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {Provider} from 'react-redux';
|
||||
import {createStore} from 'redux';
|
||||
import Reducer from 'reducers';
|
||||
import Publish from 'containers/PublishTool';
|
||||
import NavBar from 'containers/NavBar';
|
||||
|
||||
let store = createStore(
|
||||
Reducer,
|
||||
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
|
||||
);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<NavBar />
|
||||
</Provider>,
|
||||
document.getElementById('react-nav-bar')
|
||||
)
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Publish />
|
||||
</Provider>,
|
||||
document.getElementById('react-publish-tool')
|
||||
)
|
7
react/components/ActiveStatusBar/index.jsx
Normal file
7
react/components/ActiveStatusBar/index.jsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
const ActiveStatusBar = () => {
|
||||
return <span className="progress-bar progress-bar--active">| </span>;
|
||||
}
|
||||
|
||||
export default ActiveStatusBar;
|
36
react/components/ExpandingTextArea/index.jsx
Normal file
36
react/components/ExpandingTextArea/index.jsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class ExpandingTextarea extends Component {
|
||||
componentDidMount () {
|
||||
this.adjustTextarea({});
|
||||
}
|
||||
|
||||
render () {
|
||||
const { onChange, ...rest } = this.props;
|
||||
return (
|
||||
<textarea
|
||||
{ ...rest }
|
||||
ref={x => this.el = x}
|
||||
onChange={ this._handleChange.bind(this) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_handleChange (event) {
|
||||
const { onChange } = this.props;
|
||||
if (onChange) onChange(event);
|
||||
this.adjustTextarea(event);
|
||||
}
|
||||
|
||||
adjustTextarea ({ target = this.el }) {
|
||||
target.style.height = 0;
|
||||
target.style.height = `${target.scrollHeight}px`;
|
||||
}
|
||||
}
|
||||
|
||||
ExpandingTextarea.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ExpandingTextarea;
|
7
react/components/InactiveStatusBar/index.jsx
Normal file
7
react/components/InactiveStatusBar/index.jsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
const InactiveStatusBar = () => {
|
||||
return <span className="progress-bar progress-bar--inactive">| </span>;
|
||||
}
|
||||
|
||||
export default InactiveStatusBar;
|
28
react/components/Logo/index.jsx
Normal file
28
react/components/Logo/index.jsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import React from 'react';
|
||||
|
||||
function Logo () {
|
||||
return (
|
||||
<svg version="1.1" id="Layer_1" x="0px" y="0px" height="24px" viewBox="0 0 80 31" enableBackground="new 0 0 80 31" className="nav-bar-logo">
|
||||
<a href="/">
|
||||
<title>Logo</title>
|
||||
<desc>Spee.ch logo</desc>
|
||||
<g id="About">
|
||||
<g id="Publish-Form-V2-_x28_filled_x29_" transform="translate(-42.000000, -23.000000)">
|
||||
<g id="Group-17" transform="translate(42.000000, 22.000000)">
|
||||
<text transform="matrix(1 0 0 1 0 20)" fontSize="25" fontFamily="Roboto">Spee<h</text>
|
||||
<g id="Group-16" transform="translate(0.000000, 30.000000)">
|
||||
<path id="Line-8" fill="none" stroke="#09F911" strokeWidth="1" strokeLinecap="square" d="M0.5,1.5h15"/>
|
||||
<path id="Line-8-Copy" fill="none" stroke="#029D74" strokeWidth="1" strokeLinecap="square" d="M16.5,1.5h15"/>
|
||||
<path id="Line-8-Copy-2" fill="none" stroke="#E35BD8" strokeWidth="1" strokeLinecap="square" d="M32.5,1.5h15"/>
|
||||
<path id="Line-8-Copy-3" fill="none" stroke="#4156C5" strokeWidth="1" strokeLinecap="square" d="M48.5,1.5h15"/>
|
||||
<path id="Line-8-Copy-4" fill="none" stroke="#635688" strokeWidth="1" strokeLinecap="square" d="M64.5,1.5h15"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</a>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logo;
|
13
react/components/NavBarChannelOptionsDropdown/index.jsx
Normal file
13
react/components/NavBarChannelOptionsDropdown/index.jsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
|
||||
function NavBarChannelOptionsDropdown ({ channelName, handleSelection, VIEW, LOGOUT }) {
|
||||
return (
|
||||
<select type="text" id="nav-bar-channel-select" className="select select--arrow link--nav" onChange={handleSelection}>
|
||||
<option id="nav-bar-channel-select-channel-option">{channelName}</option>
|
||||
<option value={VIEW}>View</option>
|
||||
<option value={LOGOUT}>Logout</option>
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBarChannelOptionsDropdown;
|
54
react/components/Preview/index.jsx
Normal file
54
react/components/Preview/index.jsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class Preview extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
imgSource : '',
|
||||
defaultThumbnail: '/assets/img/video_thumb_default.png',
|
||||
};
|
||||
this.previewFile = this.previewFile.bind(this);
|
||||
}
|
||||
componentDidMount () {
|
||||
this.previewFile(this.props.file);
|
||||
}
|
||||
componentWillReceiveProps (newProps) {
|
||||
if (newProps.file !== this.props.file) {
|
||||
this.previewFile(newProps.file);
|
||||
}
|
||||
if (newProps.thumbnail !== this.props.thumbnail) {
|
||||
this.setState({imgSource: (newProps.thumbnail || this.state.defaultThumbnail)});
|
||||
}
|
||||
}
|
||||
previewFile (file) {
|
||||
const that = this;
|
||||
if (file.type !== 'video/mp4') {
|
||||
const previewReader = new FileReader();
|
||||
previewReader.readAsDataURL(file);
|
||||
previewReader.onloadend = function () {
|
||||
that.setState({imgSource: previewReader.result});
|
||||
};
|
||||
} else {
|
||||
that.setState({imgSource: (this.props.thumbnail || this.state.defaultThumbnail)});
|
||||
}
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<img
|
||||
id="asset-preview"
|
||||
src={this.state.imgSource}
|
||||
className={this.props.dimPreview ? 'dim' : ''}
|
||||
alt="publish preview"
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Preview.propTypes = {
|
||||
dimPreview: PropTypes.bool.isRequired,
|
||||
file : PropTypes.object.isRequired,
|
||||
thumbnail : PropTypes.string,
|
||||
};
|
||||
|
||||
export default Preview;
|
76
react/components/ProgressBar/index.jsx
Normal file
76
react/components/ProgressBar/index.jsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ActiveStatusBar from 'components/ActiveStatusBar';
|
||||
import InactiveStatusBar from 'components/InactiveStatusBar';
|
||||
|
||||
class ProgressBar extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
bars : [],
|
||||
index : 0,
|
||||
incrementer: 1,
|
||||
};
|
||||
this.createBars = this.createBars.bind(this);
|
||||
this.startProgressBar = this.startProgressBar.bind(this);
|
||||
this.updateProgressBar = this.updateProgressBar.bind(this);
|
||||
this.stopProgressBar = this.stopProgressBar.bind(this);
|
||||
}
|
||||
componentDidMount () {
|
||||
this.createBars();
|
||||
this.startProgressBar();
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.stopProgressBar();
|
||||
}
|
||||
createBars () {
|
||||
const bars = [];
|
||||
for (let i = 0; i <= this.props.size; i++) {
|
||||
bars.push({isActive: false});
|
||||
}
|
||||
this.setState({ bars });
|
||||
}
|
||||
startProgressBar () {
|
||||
this.updateInterval = setInterval(this.updateProgressBar.bind(this), 300);
|
||||
};
|
||||
updateProgressBar () {
|
||||
let index = this.state.index;
|
||||
let incrementer = this.state.incrementer;
|
||||
let bars = this.state.bars;
|
||||
// flip incrementer if necessary, to stay in bounds
|
||||
if ((index < 0) || (index > this.props.size)) {
|
||||
incrementer = incrementer * -1;
|
||||
index += incrementer;
|
||||
}
|
||||
// update the indexed bar
|
||||
if (incrementer > 0) {
|
||||
bars[index].isActive = true;
|
||||
} else {
|
||||
bars[index].isActive = false;
|
||||
};
|
||||
// increment index
|
||||
index += incrementer;
|
||||
// update state
|
||||
this.setState({
|
||||
bars,
|
||||
incrementer,
|
||||
index,
|
||||
});
|
||||
};
|
||||
stopProgressBar () {
|
||||
clearInterval(this.updateInterval);
|
||||
};
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
{this.state.bars.map((bar, index) => bar.isActive ? <ActiveStatusBar key={index} /> : <InactiveStatusBar key={index}/>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ProgressBar.propTypes = {
|
||||
size: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default ProgressBar;
|
52
react/components/PublishStatus/index.jsx
Normal file
52
react/components/PublishStatus/index.jsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ProgressBar from 'components/ProgressBar';
|
||||
import * as publishStates from 'constants/publish_claim_states';
|
||||
|
||||
function PublishStatus ({ status, message }) {
|
||||
return (
|
||||
<div className="row row--tall flex-container--column flex-container--center-center">
|
||||
{(status === publishStates.LOAD_START) &&
|
||||
<div className="row align-content-center">
|
||||
<p>File is loading to server</p>
|
||||
<p className="blue">{message}</p>
|
||||
</div>
|
||||
}
|
||||
{(status === publishStates.LOADING) &&
|
||||
<div>
|
||||
<div className="row align-content-center">
|
||||
<p>File is loading to server</p>
|
||||
<p className="blue">{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{(status === publishStates.PUBLISHING) &&
|
||||
<div className="row align-content-center">
|
||||
<p>Upload complete. Your file is now being published on the blockchain...</p>
|
||||
<ProgressBar size={12}/>
|
||||
<p>Curious what magic is happening here? <a className="link--primary" target="blank" href="https://lbry.io/faq/what-is-lbry">Learn more.</a></p>
|
||||
</div>
|
||||
}
|
||||
{(status === publishStates.SUCCESS) &&
|
||||
<div className="row align-content-center">
|
||||
<p>Your publish is complete! You are being redirected to it now.</p>
|
||||
<p>If you are not automatically redirected, <a class="link--primary" target="_blank" href={message}>click here.</a></p>
|
||||
</div>
|
||||
}
|
||||
{(status === publishStates.FAILED) &&
|
||||
<div className="row align-content-center">
|
||||
<p>Something went wrong...</p>
|
||||
<p><strong>{message}</strong></p>
|
||||
<p>For help, post the above error text in the #speech channel on the <a className="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">lbry discord</a></p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PublishStatus.propTypes = {
|
||||
status : PropTypes.string.isRequired,
|
||||
message: PropTypes.string,
|
||||
};
|
||||
|
||||
export default PublishStatus;
|
23
react/components/PublishUrlMiddleDisplay/index.jsx
Normal file
23
react/components/PublishUrlMiddleDisplay/index.jsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function UrlMiddle ({publishInChannel, selectedChannel, loggedInChannelName, loggedInChannelShortId}) {
|
||||
if (publishInChannel) {
|
||||
if (selectedChannel === loggedInChannelName) {
|
||||
return <span id="url-channel" className="url-text--secondary">{loggedInChannelName}:{loggedInChannelShortId} /</span>;
|
||||
}
|
||||
return <span id="url-channel-placeholder" className="url-text--secondary tooltip">@channel<span
|
||||
className="tooltip-text">Select a channel below</span> /</span>;
|
||||
}
|
||||
return (
|
||||
<span id="url-no-channel-placeholder" className="url-text--secondary tooltip">xyz<span className="tooltip-text">This will be a random id</span> /</span>
|
||||
);
|
||||
}
|
||||
|
||||
UrlMiddle.propTypes = {
|
||||
publishInChannel : PropTypes.bool.isRequired,
|
||||
loggedInChannelName : PropTypes.string,
|
||||
loggedInChannelShortId: PropTypes.string,
|
||||
};
|
||||
|
||||
export default UrlMiddle;
|
1
react/constants/channel_action_types.js
Normal file
1
react/constants/channel_action_types.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const CHANNEL_UPDATE = 'CHANNEL_UPDATE';
|
9
react/constants/publish_action_types.js
Normal file
9
react/constants/publish_action_types.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
export const FILE_SELECTED = 'FILE_SELECTED';
|
||||
export const FILE_CLEAR = 'FILE_CLEAR';
|
||||
export const METADATA_UPDATE = 'METADATA_UPDATE';
|
||||
export const CLAIM_UPDATE = 'CLAIM_UPDATE';
|
||||
export const SET_PUBLISH_IN_CHANNEL = 'SET_PUBLISH_IN_CHANNEL';
|
||||
export const PUBLISH_STATUS_UPDATE = 'PUBLISH_STATUS_UPDATE';
|
||||
export const ERROR_UPDATE = 'ERROR_UPDATE';
|
||||
export const SELECTED_CHANNEL_UPDATE = 'SELECTED_CHANNEL_UPDATE';
|
||||
export const TOGGLE_METADATA_INPUTS = 'TOGGLE_METADATA_INPUTS';
|
2
react/constants/publish_channel_select_states.js
Normal file
2
react/constants/publish_channel_select_states.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const LOGIN = 'Existing';
|
||||
export const CREATE = 'New';
|
5
react/constants/publish_claim_states.js
Normal file
5
react/constants/publish_claim_states.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export const LOAD_START = 'LOAD_START';
|
||||
export const LOADING = 'LOADING';
|
||||
export const PUBLISHING = 'PUBLISHING';
|
||||
export const SUCCESS = 'SUCCESS';
|
||||
export const FAILED = 'FAILED';
|
15
react/containers/ChannelCreateForm/index.js
Normal file
15
react/containers/ChannelCreateForm/index.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { updateLoggedInChannel } from 'actions/channel';
|
||||
import View from './view';
|
||||
import {updateSelectedChannel} from '../../actions/publish';
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onChannelLogin: (name, shortId, longId) => {
|
||||
dispatch(updateLoggedInChannel(name, shortId, longId));
|
||||
dispatch(updateSelectedChannel(name));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps)(View);
|
168
react/containers/ChannelCreateForm/view.jsx
Normal file
168
react/containers/ChannelCreateForm/view.jsx
Normal file
|
@ -0,0 +1,168 @@
|
|||
import React from 'react';
|
||||
import ProgressBar from 'components/ProgressBar';
|
||||
import request from 'utils/request';
|
||||
|
||||
class ChannelCreateForm extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error : null,
|
||||
channel : '',
|
||||
password: '',
|
||||
status : null,
|
||||
};
|
||||
this.cleanseChannelInput = this.cleanseChannelInput.bind(this);
|
||||
this.handleChannelInput = this.handleChannelInput.bind(this);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.updateIsChannelAvailable = this.updateIsChannelAvailable.bind(this);
|
||||
this.checkIsChannelAvailable = this.checkIsChannelAvailable.bind(this);
|
||||
this.checkIsPasswordProvided = this.checkIsPasswordProvided.bind(this);
|
||||
this.makePublishChannelRequest = this.makePublishChannelRequest.bind(this);
|
||||
this.createChannel = this.createChannel.bind(this);
|
||||
}
|
||||
cleanseChannelInput (input) {
|
||||
input = input.replace(/\s+/g, '-'); // replace spaces with dashes
|
||||
input = input.replace(/[^A-Za-z0-9-]/g, ''); // remove all characters that are not A-Z, a-z, 0-9, or '-'
|
||||
return input;
|
||||
}
|
||||
handleChannelInput (event) {
|
||||
let value = event.target.value;
|
||||
value = this.cleanseChannelInput(value);
|
||||
this.setState({channel: value});
|
||||
if (value) {
|
||||
this.updateIsChannelAvailable(value);
|
||||
} else {
|
||||
this.setState({error: 'Please enter a channel name'});
|
||||
}
|
||||
}
|
||||
handleInput (event) {
|
||||
const name = event.target.name;
|
||||
const value = event.target.value;
|
||||
this.setState({[name]: value});
|
||||
}
|
||||
updateIsChannelAvailable (channel) {
|
||||
const that = this;
|
||||
const channelWithAtSymbol = `@${channel}`;
|
||||
request(`/api/channel-is-available/${channelWithAtSymbol}`)
|
||||
.then(isAvailable => {
|
||||
if (isAvailable) {
|
||||
that.setState({'error': null});
|
||||
} else {
|
||||
that.setState({'error': 'That channel has already been claimed'});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
that.setState({'error': error.message});
|
||||
});
|
||||
}
|
||||
checkIsChannelAvailable (channel) {
|
||||
const channelWithAtSymbol = `@${channel}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
request(`/api/channel-is-available/${channelWithAtSymbol}`)
|
||||
.then(isAvailable => {
|
||||
console.log('checkIsChannelAvailable result:', isAvailable);
|
||||
if (!isAvailable) {
|
||||
return reject(new Error('That channel has already been claimed'));
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
checkIsPasswordProvided () {
|
||||
const password = this.state.password;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!password || password.length < 1) {
|
||||
console.log('password not provided');
|
||||
return reject(new Error('Please provide a password'));
|
||||
}
|
||||
console.log('password provided');
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
makePublishChannelRequest (username, password) {
|
||||
const params = {
|
||||
method : 'POST',
|
||||
body : JSON.stringify({username, password}),
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
credentials: 'include',
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
request('/signup', params)
|
||||
.then(result => {
|
||||
console.log('makePublishChannelRequest result:', result);
|
||||
return resolve(result);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('create channel request failed:', error);
|
||||
reject(new Error('Unfortunately, we encountered an error while creating your channel. Please let us know in Discord!'));
|
||||
});
|
||||
});
|
||||
}
|
||||
createChannel (event) {
|
||||
event.preventDefault();
|
||||
const that = this;
|
||||
this.checkIsPasswordProvided()
|
||||
.then(() => {
|
||||
return that.checkIsChannelAvailable(that.state.channel, that.state.password);
|
||||
})
|
||||
.then(() => {
|
||||
that.setState({status: 'We are publishing your new channel. Sit tight...'});
|
||||
return that.makePublishChannelRequest(that.state.channel, that.state.password);
|
||||
})
|
||||
.then(result => {
|
||||
that.setState({status: null});
|
||||
that.props.onChannelLogin(result.channelName, result.shortChannelId, result.channelClaimId);
|
||||
})
|
||||
.catch((error) => {
|
||||
that.setState({'error': error.message, status: null});
|
||||
});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
{ !this.state.status ? (
|
||||
<form id="publish-channel-form">
|
||||
<p id="input-error-channel-name" className="info-message-placeholder info-message--failure">{this.state.error}</p>
|
||||
<div className="row row--wide row--short">
|
||||
<div className="column column--3 column--sml-10">
|
||||
<label className="label" htmlFor="new-channel-name">Name:</label>
|
||||
</div><div className="column column--6 column--sml-10">
|
||||
<div className="input-text--primary flex-container--row flex-container--left-bottom span--relative">
|
||||
<span>@</span>
|
||||
<input type="text" name="channel" id="new-channel-name" className="input-text" placeholder="exampleChannelName" value={this.state.channel} onChange={this.handleChannelInput} />
|
||||
{ (this.state.channel && !this.state.error) && <span id="input-success-channel-name" className="info-message--success span--absolute">{'\u2713'}</span> }
|
||||
{ this.state.error && <span id="input-success-channel-name" className="info-message--failure span--absolute">{'\u2716'}</span> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row row--wide row--short">
|
||||
<div className="column column--3 column--sml-10">
|
||||
<label className="label" htmlFor="new-channel-password">Password:</label>
|
||||
</div><div className="column column--6 column--sml-10">
|
||||
<div className="input-text--primary">
|
||||
<input type="password" name="password" id="new-channel-password" className="input-text" placeholder="" value={this.state.password} onChange={this.handleInput} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row row--wide">
|
||||
<button className="button--primary" onClick={this.createChannel}>Create Channel</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div>
|
||||
<p className="fine-print">{this.state.status}</p>
|
||||
<ProgressBar size={12}/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelCreateForm;
|
15
react/containers/ChannelLoginForm/index.js
Normal file
15
react/containers/ChannelLoginForm/index.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { updateLoggedInChannel } from 'actions/channel';
|
||||
import View from './view';
|
||||
import {updateSelectedChannel} from '../../actions/publish';
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onChannelLogin: (name, shortId, longId) => {
|
||||
dispatch(updateLoggedInChannel(name, shortId, longId));
|
||||
dispatch(updateSelectedChannel(name));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps)(View);
|
81
react/containers/ChannelLoginForm/view.jsx
Normal file
81
react/containers/ChannelLoginForm/view.jsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
import React from 'react';
|
||||
import request from 'utils/request';
|
||||
|
||||
class ChannelLoginForm extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error : null,
|
||||
name : '',
|
||||
password: '',
|
||||
};
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.loginToChannel = this.loginToChannel.bind(this);
|
||||
}
|
||||
handleInput (event) {
|
||||
const name = event.target.name;
|
||||
const value = event.target.value;
|
||||
this.setState({[name]: value});
|
||||
}
|
||||
loginToChannel (event) {
|
||||
event.preventDefault();
|
||||
const params = {
|
||||
method : 'POST',
|
||||
body : JSON.stringify({username: this.state.name, password: this.state.password}),
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
credentials: 'include',
|
||||
}
|
||||
const that = this;
|
||||
request('login', params)
|
||||
.then(({success, channelName, shortChannelId, channelClaimId, message}) => {
|
||||
console.log('loginToChannel success:', success);
|
||||
if (success) {
|
||||
that.props.onChannelLogin(channelName, shortChannelId, channelClaimId);
|
||||
} else {
|
||||
that.setState({'error': message});
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('login error', error);
|
||||
if (error.message) {
|
||||
that.setState({'error': error.message});
|
||||
} else {
|
||||
that.setState({'error': error});
|
||||
}
|
||||
});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<form id="channel-login-form">
|
||||
<p id="login-error-display-element" className="info-message-placeholder info-message--failure">{this.state.error}</p>
|
||||
<div className="row row--wide row--short">
|
||||
<div className="column column--3 column--sml-10">
|
||||
<label className="label" htmlFor="channel-login-name-input">Name:</label>
|
||||
</div><div className="column column--6 column--sml-10">
|
||||
<div className="input-text--primary flex-container--row flex-container--left-bottom">
|
||||
<span>@</span>
|
||||
<input type="text" id="channel-login-name-input" className="input-text" name="name" placeholder="Your Channel Name" value={this.state.channelName} onChange={this.handleInput}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row row--wide row--short">
|
||||
<div className="column column--3 column--sml-10">
|
||||
<label className="label" htmlFor="channel-login-password-input" >Password:</label>
|
||||
</div><div className="column column--6 column--sml-10">
|
||||
<div className="input-text--primary">
|
||||
<input type="password" id="channel-login-password-input" name="password" className="input-text" placeholder="" value={this.state.channelPassword} onChange={this.handleInput}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row row--wide">
|
||||
<button className="button--primary" onClick={this.loginToChannel}>Authenticate</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelLoginForm;
|
27
react/containers/ChannelSelect/index.js
Normal file
27
react/containers/ChannelSelect/index.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import {connect} from 'react-redux';
|
||||
import {setPublishInChannel, updateSelectedChannel, updateError} from 'actions/publish';
|
||||
import View from './view.jsx';
|
||||
|
||||
const mapStateToProps = ({ channel, publish }) => {
|
||||
return {
|
||||
loggedInChannelName: channel.loggedInChannel.name,
|
||||
publishInChannel : publish.publishInChannel,
|
||||
selectedChannel : publish.selectedChannel,
|
||||
channelError : publish.error.channel,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onPublishInChannelChange: (value) => {
|
||||
dispatch(updateError('channel', null));
|
||||
dispatch(setPublishInChannel(value));
|
||||
},
|
||||
onChannelSelect: (value) => {
|
||||
dispatch(updateError('channel', null));
|
||||
dispatch(updateSelectedChannel(value));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
58
react/containers/ChannelSelect/view.jsx
Normal file
58
react/containers/ChannelSelect/view.jsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
import ChannelLoginForm from 'containers/ChannelLoginForm';
|
||||
import ChannelCreateForm from 'containers/ChannelCreateForm';
|
||||
import * as states from 'constants/publish_channel_select_states';
|
||||
|
||||
class ChannelSelect extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.toggleAnonymousPublish = this.toggleAnonymousPublish.bind(this);
|
||||
this.handleSelection = this.handleSelection.bind(this);
|
||||
}
|
||||
toggleAnonymousPublish (event) {
|
||||
const value = event.target.value;
|
||||
if (value === 'anonymous') {
|
||||
this.props.onPublishInChannelChange(false);
|
||||
} else {
|
||||
this.props.onPublishInChannelChange(true);
|
||||
}
|
||||
}
|
||||
handleSelection (event) {
|
||||
const selectedOption = event.target.selectedOptions[0].value;
|
||||
this.props.onChannelSelect(selectedOption);
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<p id="input-error-channel-select" className="info-message-placeholder info-message--failure">{this.props.channelError}</p>
|
||||
<form>
|
||||
<div className="column column--3 column--med-10">
|
||||
<input type="radio" name="anonymous-or-channel" id="anonymous-radio" className="input-radio" value="anonymous" checked={!this.props.publishInChannel} onChange={this.toggleAnonymousPublish}/>
|
||||
<label className="label label--pointer" htmlFor="anonymous-radio">Anonymous</label>
|
||||
</div>
|
||||
<div className="column column--7 column--med-10">
|
||||
<input type="radio" name="anonymous-or-channel" id="channel-radio" className="input-radio" value="in a channel" checked={this.props.publishInChannel} onChange={this.toggleAnonymousPublish}/>
|
||||
<label className="label label--pointer" htmlFor="channel-radio">In a channel</label>
|
||||
</div>
|
||||
</form>
|
||||
{ this.props.publishInChannel && (
|
||||
<div>
|
||||
<div className="column column--3">
|
||||
<label className="label" htmlFor="channel-name-select">Channel:</label>
|
||||
</div><div className="column column--7">
|
||||
<select type="text" id="channel-name-select" className="select select--arrow" value={this.props.selectedChannel} onChange={this.handleSelection}>
|
||||
{ this.props.loggedInChannelName && <option value={this.props.loggedInChannelName} id="publish-channel-select-channel-option">{this.props.loggedInChannelName}</option> }
|
||||
<option value={states.LOGIN}>Existing</option>
|
||||
<option value={states.CREATE}>New</option>
|
||||
</select>
|
||||
</div>
|
||||
{ (this.props.selectedChannel === states.LOGIN) && <ChannelLoginForm /> }
|
||||
{ (this.props.selectedChannel === states.CREATE) && <ChannelCreateForm /> }
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelSelect;
|
25
react/containers/Dropzone/index.js
Normal file
25
react/containers/Dropzone/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectFile, updateError } from 'actions/publish';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ publish }) => {
|
||||
return {
|
||||
file : publish.file,
|
||||
thumbnail: publish.metadata.thumbnail,
|
||||
fileError: publish.error.file,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onFileSelect: (file) => {
|
||||
dispatch(selectFile(file));
|
||||
dispatch(updateError('publishSubmit', null));
|
||||
},
|
||||
onFileError: (value) => {
|
||||
dispatch(updateError('file', value));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
140
react/containers/Dropzone/view.jsx
Normal file
140
react/containers/Dropzone/view.jsx
Normal file
|
@ -0,0 +1,140 @@
|
|||
import React from 'react';
|
||||
import { validateFile } from 'utils/file';
|
||||
import Preview from 'components/Preview';
|
||||
|
||||
class Dropzone extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
dragOver : false,
|
||||
mouseOver : false,
|
||||
dimPreview: false,
|
||||
};
|
||||
this.handleDrop = this.handleDrop.bind(this);
|
||||
this.handleDragOver = this.handleDragOver.bind(this);
|
||||
this.handleDragEnd = this.handleDragEnd.bind(this);
|
||||
this.handleDragEnter = this.handleDragEnter.bind(this);
|
||||
this.handleDragLeave = this.handleDragLeave.bind(this);
|
||||
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleFileInput = this.handleFileInput.bind(this);
|
||||
this.selectFile = this.selectFile.bind(this);
|
||||
}
|
||||
handleDrop (event) {
|
||||
event.preventDefault();
|
||||
this.setState({dragOver: false});
|
||||
// if dropped items aren't files, reject them
|
||||
const dt = event.dataTransfer;
|
||||
console.log('dt', dt);
|
||||
if (dt.items) {
|
||||
if (dt.items[0].kind == 'file') {
|
||||
const droppedFile = dt.items[0].getAsFile();
|
||||
this.selectFile(droppedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
handleDragOver (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
handleDragEnd (event) {
|
||||
var dt = event.dataTransfer;
|
||||
if (dt.items) {
|
||||
for (var i = 0; i < dt.items.length; i++) {
|
||||
dt.items.remove(i);
|
||||
}
|
||||
} else {
|
||||
event.dataTransfer.clearData();
|
||||
}
|
||||
}
|
||||
handleDragEnter () {
|
||||
this.setState({dragOver: true, dimPreview: true});
|
||||
}
|
||||
handleDragLeave () {
|
||||
this.setState({dragOver: false, dimPreview: false});
|
||||
}
|
||||
handleMouseEnter () {
|
||||
this.setState({mouseOver: true, dimPreview: true});
|
||||
}
|
||||
handleMouseLeave () {
|
||||
this.setState({mouseOver: false, dimPreview: false});
|
||||
}
|
||||
handleClick (event) {
|
||||
event.preventDefault();
|
||||
// trigger file input
|
||||
document.getElementById('file_input').click();
|
||||
}
|
||||
handleFileInput (event) {
|
||||
event.preventDefault();
|
||||
const fileList = event.target.files;
|
||||
this.selectFile(fileList[0]);
|
||||
}
|
||||
selectFile (file) {
|
||||
if (file) {
|
||||
try {
|
||||
validateFile(file); // validate the file's name, type, and size
|
||||
} catch (error) {
|
||||
return this.props.onFileError(error.message);
|
||||
}
|
||||
// stage it so it will be ready when the publish button is clicked
|
||||
this.props.onFileError(null);
|
||||
this.props.onFileSelect(file);
|
||||
}
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div className="row row--tall flex-container--column">
|
||||
<form>
|
||||
<input className="input-file" type="file" id="file_input" name="file_input" accept="video/*,image/*" onChange={this.handleFileInput} encType="multipart/form-data"/>
|
||||
</form>
|
||||
<div id="preview-dropzone" className={'row row--padded row--tall dropzone' + (this.state.dragOver ? ' dropzone--drag-over' : '')} onDrop={this.handleDrop} onDragOver={this.handleDragOver} onDragEnd={this.handleDragEnd} onDragEnter={this.handleDragEnter} onDragLeave={this.handleDragLeave} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick}>
|
||||
{this.props.file ? (
|
||||
<div>
|
||||
<Preview
|
||||
dimPreview={this.state.dimPreview}
|
||||
file={this.props.file}
|
||||
thumbnail={this.props.thumbnail}
|
||||
/>
|
||||
<div id="dropzone-text-holder" className={'flex-container--column flex-container--center-center'}>
|
||||
{ this.state.dragOver ? (
|
||||
<div id="dropzone-dragover">
|
||||
<p className="blue">Drop it.</p>
|
||||
</div>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
{ this.state.mouseOver ? (
|
||||
<div id="dropzone-instructions">
|
||||
<p className="info-message-placeholder info-message--failure" id="input-error-file-selection">{this.props.fileError}</p>
|
||||
<p>Drag & drop image or video here to publish</p>
|
||||
<p className="fine-print">OR</p>
|
||||
<p className="blue--underlined">CHOOSE FILE</p>
|
||||
</div>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div id="dropzone-text-holder" className={'flex-container--column flex-container--center-center'}>
|
||||
{ this.state.dragOver ? (
|
||||
<div id="dropzone-dragover">
|
||||
<p className="blue">Drop it.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div id="dropzone-instructions">
|
||||
<p className="info-message-placeholder info-message--failure" id="input-error-file-selection">{this.props.fileError}</p>
|
||||
<p>Drag & drop image or video here to publish</p>
|
||||
<p className="fine-print">OR</p>
|
||||
<p className="blue--underlined">CHOOSE FILE</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Dropzone;
|
23
react/containers/NavBar/index.js
Normal file
23
react/containers/NavBar/index.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { updateLoggedInChannel } from 'actions/channel';
|
||||
import View from './view';
|
||||
import {updateSelectedChannel} from '../../actions/publish';
|
||||
|
||||
const mapStateToProps = ({ channel }) => {
|
||||
return {
|
||||
channelName : channel.loggedInChannel.name,
|
||||
channelShortId: channel.loggedInChannel.shortId,
|
||||
channelLongId : channel.loggedInChannel.longId,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onChannelLogin: (name, shortId, longId) => {
|
||||
dispatch(updateLoggedInChannel(name, shortId, longId));
|
||||
dispatch(updateSelectedChannel(name));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
85
react/containers/NavBar/view.jsx
Normal file
85
react/containers/NavBar/view.jsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
import React from 'react';
|
||||
import request from 'utils/request';
|
||||
import Logo from 'components/Logo';
|
||||
import NavBarChannelDropdown from 'components/NavBarChannelOptionsDropdown';
|
||||
|
||||
const VIEW = 'VIEW';
|
||||
const LOGOUT = 'LOGOUT';
|
||||
|
||||
class NavBar extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.checkForLoggedInUser = this.checkForLoggedInUser.bind(this);
|
||||
this.logoutUser = this.logoutUser.bind(this);
|
||||
this.handleSelection = this.handleSelection.bind(this);
|
||||
}
|
||||
componentDidMount () {
|
||||
// check to see if the user is already logged in
|
||||
this.checkForLoggedInUser();
|
||||
}
|
||||
checkForLoggedInUser () {
|
||||
// check for whether a channel is already logged in
|
||||
const params = {
|
||||
credentials: 'include',
|
||||
}
|
||||
request('/user', params)
|
||||
.then(({success, message}) => {
|
||||
if (success) {
|
||||
this.props.onChannelLogin(message.channelName, message.shortChannelId, message.channelClaimId);
|
||||
} else {
|
||||
console.log('user was not logged in');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('authenticate user errored:', error);
|
||||
});
|
||||
}
|
||||
logoutUser () {
|
||||
// send logout request to server
|
||||
window.location.href = '/logout'; // NOTE: replace with a call to the server
|
||||
}
|
||||
handleSelection (event) {
|
||||
console.log('handling selection', event);
|
||||
const value = event.target.selectedOptions[0].value;
|
||||
console.log('value', value);
|
||||
switch (value) {
|
||||
case LOGOUT:
|
||||
this.logoutUser();
|
||||
break;
|
||||
case VIEW:
|
||||
// redirect to channel page
|
||||
window.location.href = `/${this.props.channelName}:${this.props.channelLongId}`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div className="row row--wide nav-bar">
|
||||
<div className="row row--padded row--short flex-container--row flex-container--space-between-center">
|
||||
<Logo />
|
||||
<div className="nav-bar--center">
|
||||
<span className="nav-bar-tagline">Open-source, decentralized image and video sharing.</span>
|
||||
</div>
|
||||
<div className="nav-bar--right">
|
||||
<a className="nav-bar-link link--nav-active" href="/">Publish</a>
|
||||
<a className="nav-bar-link link--nav" href="/about">About</a>
|
||||
{ this.props.channelName ? (
|
||||
<NavBarChannelDropdown
|
||||
channelName={this.props.channelName}
|
||||
handleSelection={this.handleSelection}
|
||||
VIEW={VIEW}
|
||||
LOGOUT={LOGOUT}
|
||||
/>
|
||||
) : (
|
||||
<a id="nav-bar-login-link" className="nav-bar-link link--nav" href="/login">Channel</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default NavBar;
|
47
react/containers/PublishForm/index.js
Normal file
47
react/containers/PublishForm/index.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import {connect} from 'react-redux';
|
||||
import {clearFile, selectFile, updateError, updatePublishStatus} from 'actions/publish';
|
||||
import {updateLoggedInChannel} from 'actions/channel';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ channel, publish }) => {
|
||||
return {
|
||||
loggedInChannel : channel.loggedInChannel,
|
||||
file : publish.file,
|
||||
claim : publish.claim,
|
||||
title : publish.metadata.title,
|
||||
thumbnail : publish.metadata.thumbnail,
|
||||
description : publish.metadata.description,
|
||||
license : publish.metadata.license,
|
||||
nsfw : publish.metadata.nsfw,
|
||||
publishInChannel : publish.publishInChannel,
|
||||
selectedChannel : publish.selectedChannel,
|
||||
fileError : publish.error.file,
|
||||
urlError : publish.error.url,
|
||||
publishSubmitError: publish.error.publishSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onFileSelect: (file) => {
|
||||
dispatch(selectFile(file));
|
||||
},
|
||||
onFileClear: () => {
|
||||
dispatch(clearFile());
|
||||
},
|
||||
onChannelLogin: (name, shortId, longId) => {
|
||||
dispatch(updateLoggedInChannel(name, shortId, longId));
|
||||
},
|
||||
onPublishStatusChange: (status, message) => {
|
||||
dispatch(updatePublishStatus(status, message));
|
||||
},
|
||||
onChannelSelectionError: (value) => {
|
||||
dispatch(updateError('channel', value));
|
||||
},
|
||||
onPublishSubmitError: (value) => {
|
||||
dispatch(updateError('publishSubmit', value));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
179
react/containers/PublishForm/view.jsx
Normal file
179
react/containers/PublishForm/view.jsx
Normal file
|
@ -0,0 +1,179 @@
|
|||
import React from 'react';
|
||||
import Dropzone from 'containers/Dropzone';
|
||||
import PublishTitleInput from 'containers/PublishTitleInput';
|
||||
import PublishUrlInput from 'containers/PublishUrlInput';
|
||||
import PublishThumbnailInput from 'containers/PublishThumbnailInput';
|
||||
import PublishMetadataInputs from 'containers/PublishMetadataInputs';
|
||||
import ChannelSelect from 'containers/ChannelSelect';
|
||||
import * as publishStates from 'constants/publish_claim_states';
|
||||
|
||||
class PublishForm extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.validateChannelSelection = this.validateChannelSelection.bind(this);
|
||||
this.validatePublishParams = this.validatePublishParams.bind(this);
|
||||
this.makePublishRequest = this.makePublishRequest.bind(this);
|
||||
this.publish = this.publish.bind(this);
|
||||
}
|
||||
validateChannelSelection () {
|
||||
console.log('validating channel selection');
|
||||
// make sure all required data is provided
|
||||
return new Promise((resolve, reject) => {
|
||||
// if publishInChannel is true, is a channel selected & logged in?
|
||||
if (this.props.publishInChannel && (this.props.selectedChannel !== this.props.loggedInChannel.name)) {
|
||||
// update state with error
|
||||
this.props.onChannelSelectionError('Log in to a channel or select Anonymous"');
|
||||
// reject this promise
|
||||
return reject(new Error('Fix the channel'));
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
validatePublishParams () {
|
||||
console.log('validating publish params');
|
||||
// make sure all required data is provided
|
||||
return new Promise((resolve, reject) => {
|
||||
// is there a file?
|
||||
if (!this.props.file) {
|
||||
return reject(new Error('Please choose a file'));
|
||||
}
|
||||
// is there a claim chosen?
|
||||
if (!this.props.claim) {
|
||||
return reject(new Error('Please enter a URL'));
|
||||
}
|
||||
if (this.props.urlError) {
|
||||
return reject(new Error('Fix the url'));
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
makePublishRequest (file, metadata) {
|
||||
console.log('making publish request');
|
||||
const uri = '/api/claim-publish';
|
||||
const xhr = new XMLHttpRequest();
|
||||
const fd = this.appendDataToFormData(file, metadata);
|
||||
const that = this;
|
||||
xhr.upload.addEventListener('loadstart', function () {
|
||||
that.props.onPublishStatusChange(publishStates.LOAD_START, 'upload started');
|
||||
});
|
||||
xhr.upload.addEventListener('progress', function (e) {
|
||||
if (e.lengthComputable) {
|
||||
const percentage = Math.round((e.loaded * 100) / e.total);
|
||||
console.log('progress:', percentage);
|
||||
that.props.onPublishStatusChange(publishStates.LOADING, `${percentage}%`);
|
||||
}
|
||||
}, false);
|
||||
xhr.upload.addEventListener('load', function () {
|
||||
console.log('loaded 100%');
|
||||
that.props.onPublishStatusChange(publishStates.PUBLISHING, null);
|
||||
}, false);
|
||||
xhr.open('POST', uri, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4) {
|
||||
console.log('publish response:', xhr.response);
|
||||
if (xhr.status === 200) {
|
||||
console.log('publish complete!');
|
||||
const url = JSON.parse(xhr.response).message.url;
|
||||
that.props.onPublishStatusChange(publishStates.SUCCESS, url);
|
||||
window.location = url;
|
||||
} else if (xhr.status === 502) {
|
||||
that.props.onPublishStatusChange(publishStates.FAILED, 'Spee.ch was not able to get a response from the LBRY network.');
|
||||
} else {
|
||||
that.props.onPublishStatusChange(publishStates.FAILED, JSON.parse(xhr.response).message);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Initiate a multipart/form-data upload
|
||||
xhr.send(fd);
|
||||
}
|
||||
createMetadata () {
|
||||
console.log('creating metadata');
|
||||
let metadata = {
|
||||
name : this.props.claim,
|
||||
title : this.props.title,
|
||||
description: this.props.description,
|
||||
license : this.props.license,
|
||||
nsfw : this.props.nsfw,
|
||||
type : this.props.file.type,
|
||||
thumbnail : this.props.thumbnail,
|
||||
};
|
||||
if (this.props.publishInChannel) {
|
||||
metadata['channelName'] = this.props.selectedChannel;
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
appendDataToFormData (file, metadata) {
|
||||
var fd = new FormData();
|
||||
fd.append('file', file);
|
||||
for (var key in metadata) {
|
||||
if (metadata.hasOwnProperty(key)) {
|
||||
console.log('adding form data', key, metadata[key]);
|
||||
fd.append(key, metadata[key]);
|
||||
}
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
publish () {
|
||||
console.log('publishing file');
|
||||
// publish the asset
|
||||
const that = this;
|
||||
this.validateChannelSelection()
|
||||
.then(() => {
|
||||
return that.validatePublishParams();
|
||||
})
|
||||
.then(() => {
|
||||
const metadata = that.createMetadata();
|
||||
// publish the claim
|
||||
return that.makePublishRequest(that.props.file, metadata);
|
||||
})
|
||||
.then(() => {
|
||||
that.props.onPublishStatusChange('publish request made');
|
||||
})
|
||||
.catch((error) => {
|
||||
that.props.onPublishSubmitError(error.message);
|
||||
});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div className="row row--no-bottom">
|
||||
<div className="column column--10">
|
||||
<PublishTitleInput />
|
||||
</div>
|
||||
<div className="column column--5 column--sml-10" >
|
||||
<div className="row row--padded">
|
||||
<Dropzone />
|
||||
</div>
|
||||
</div>
|
||||
<div className="column column--5 column--sml-10 align-content-top">
|
||||
<div id="publish-active-area" className="row row--padded">
|
||||
<div className="row row--padded row--no-top row--wide">
|
||||
<PublishUrlInput />
|
||||
</div>
|
||||
<div className="row row--padded row--no-top row--wide">
|
||||
<ChannelSelect />
|
||||
</div>
|
||||
{ (this.props.file.type === 'video/mp4') && (
|
||||
<div className="row row--padded row--no-top row--wide ">
|
||||
<PublishThumbnailInput />
|
||||
</div>
|
||||
)}
|
||||
<div className="row row--padded row--no-top row--no-bottom row--wide">
|
||||
<PublishMetadataInputs />
|
||||
</div>
|
||||
<div className="row row--wide align-content-center">
|
||||
<button id="publish-submit" className="button--primary button--large" onClick={this.publish}>Publish</button>
|
||||
</div>
|
||||
<div className="row row--padded row--no-bottom align-content-center">
|
||||
<button className="button--cancel" onClick={this.props.onFileClear}>Cancel</button>
|
||||
</div>
|
||||
<div className="row row--short align-content-center">
|
||||
<p className="fine-print">By clicking 'Publish', you affirm that you have the rights to publish this content to the LBRY network, and that you understand the properties of publishing it to a decentralized, user-controlled network. <a className="link--primary" target="_blank" href="https://lbry.io/learn">Read more.</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default PublishForm;
|
25
react/containers/PublishMetadataInputs/index.js
Normal file
25
react/containers/PublishMetadataInputs/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import {connect} from 'react-redux';
|
||||
import {updateMetadata, toggleMetadataInputs} from 'actions/publish';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ publish }) => {
|
||||
return {
|
||||
showMetadataInputs: publish.showMetadataInputs,
|
||||
description : publish.metadata.description,
|
||||
license : publish.metadata.license,
|
||||
nsfw : publish.metadata.nsfw,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onMetadataChange: (name, value) => {
|
||||
dispatch(updateMetadata(name, value));
|
||||
},
|
||||
onToggleMetadataInputs: (value) => {
|
||||
dispatch(toggleMetadataInputs(value));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
74
react/containers/PublishMetadataInputs/view.jsx
Normal file
74
react/containers/PublishMetadataInputs/view.jsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import React from 'react';
|
||||
import ExpandingTextArea from 'components/ExpandingTextArea';
|
||||
|
||||
class PublishMetadataInputs extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.toggleShowInputs = this.toggleShowInputs.bind(this);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
}
|
||||
toggleShowInputs () {
|
||||
this.props.onToggleMetadataInputs(!this.props.showMetadataInputs);
|
||||
}
|
||||
handleInput (event) {
|
||||
const target = event.target;
|
||||
const value = target.type === 'checkbox' ? target.checked : target.value;
|
||||
const name = target.name;
|
||||
this.props.onMetadataChange(name, value);
|
||||
}
|
||||
handleSelect (event) {
|
||||
const name = event.target.name;
|
||||
const selectedOption = event.target.selectedOptions[0].value;
|
||||
this.props.onMetadataChange(name, selectedOption);
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div id="publish-details" className="row row--padded row--no-top row--wide">
|
||||
{this.props.showMetadataInputs && (
|
||||
<div>
|
||||
<div className="row row--no-top">
|
||||
<div className="column column--3 column--med-10 align-content-top">
|
||||
<label htmlFor="publish-license" className="label">Description:</label>
|
||||
</div><div className="column column--7 column--sml-10">
|
||||
<ExpandingTextArea
|
||||
id="publish-description"
|
||||
className="textarea textarea--primary textarea--full-width"
|
||||
rows={1}
|
||||
maxLength={2000}
|
||||
style={{ maxHeight: 200 }}
|
||||
name="description"
|
||||
placeholder="Optional description"
|
||||
value={this.props.description}
|
||||
onChange={this.handleInput} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row row--no-top">
|
||||
<div className="column column--3 column--med-10">
|
||||
<label htmlFor="publish-license" className="label">License:</label>
|
||||
</div><div className="column column--7 column--sml-10">
|
||||
<select type="text" name="license" id="publish-license" className="select select--primary" onChange={this.handleSelect}>
|
||||
<option value=" ">Unspecified</option>
|
||||
<option value="Public Domain">Public Domain</option>
|
||||
<option value="Creative Commons">Creative Commons</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row row--no-top">
|
||||
<div className="column column--3">
|
||||
<label htmlFor="publish-nsfw" className="label">Mature:</label>
|
||||
</div><div className="column column--7">
|
||||
<input className="input-checkbox" type="checkbox" id="publish-nsfw" name="nsfw" value={this.props.nsfw} onChange={this.handleInput} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<a className="label link--primary" id="publish-details-toggle" href="#" onClick={this.toggleShowInputs}>{this.props.showMetadataInputs ? '[less]' : '[more]'}</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PublishMetadataInputs;
|
19
react/containers/PublishThumbnailInput/index.js
Normal file
19
react/containers/PublishThumbnailInput/index.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import {connect} from 'react-redux';
|
||||
import {updateMetadata} from 'actions/publish';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ publish }) => {
|
||||
return {
|
||||
thumbnail: publish.metadata.thumbnail,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onThumbnailChange: (name, value) => {
|
||||
dispatch(updateMetadata(name, value));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
83
react/containers/PublishThumbnailInput/view.jsx
Normal file
83
react/containers/PublishThumbnailInput/view.jsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
|
||||
class PublishThumbnailInput extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
videoPreviewSrc: null,
|
||||
thumbnailError : null,
|
||||
thumbnailInput : '',
|
||||
}
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.urlIsAnImage = this.urlIsAnImage.bind(this);
|
||||
this.testImage = this.testImage.bind(this);
|
||||
this.updateVideoThumb = this.updateVideoThumb.bind(this);
|
||||
}
|
||||
handleInput (event) {
|
||||
const value = event.target.value;
|
||||
this.setState({thumbnailInput: value});
|
||||
}
|
||||
urlIsAnImage (url) {
|
||||
return (url.match(/\.(jpeg|jpg|gif|png)$/) != null);
|
||||
}
|
||||
testImage (url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhttp = new XMLHttpRequest();
|
||||
xhttp.open('HEAD', url, true);
|
||||
xhttp.onreadystatechange = () => {
|
||||
if (xhttp.readyState === 4) {
|
||||
if (xhttp.status === 200) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}
|
||||
};
|
||||
xhttp.send();
|
||||
});
|
||||
}
|
||||
updateVideoThumb (event) {
|
||||
const imageUrl = event.target.value;
|
||||
const that = this;
|
||||
if (this.urlIsAnImage(imageUrl)) {
|
||||
this.testImage(imageUrl, 3000)
|
||||
.then(() => {
|
||||
console.log('thumbnail is a valid image');
|
||||
that.props.onThumbnailChange('thumbnail', imageUrl);
|
||||
that.setState({thumbnailError: null});
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('encountered an error loading thumbnail image url:', error);
|
||||
that.props.onThumbnailChange('thumbnail', null);
|
||||
that.setState({thumbnailError: 'That is an invalid image url'});
|
||||
});
|
||||
} else {
|
||||
that.props.onThumbnailChange('thumbnail', null);
|
||||
that.setState({thumbnailError: null});
|
||||
}
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<div className="column column--3 column--sml-10">
|
||||
<label className="label">Thumbnail:</label>
|
||||
</div><div className="column column--6 column--sml-10">
|
||||
<div className="input-text--primary">
|
||||
<p className="info-message-placeholder info-message--failure">{this.state.thumbnailError}</p>
|
||||
<input
|
||||
type="text" id="claim-thumbnail-input"
|
||||
className="input-text input-text--full-width"
|
||||
placeholder="https://spee.ch/xyz/example.jpg"
|
||||
value={this.state.thumbnailInput}
|
||||
onChange={ (event) => {
|
||||
this.handleInput(event);
|
||||
this.updateVideoThumb(event);
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PublishThumbnailInput;
|
19
react/containers/PublishTitleInput/index.js
Normal file
19
react/containers/PublishTitleInput/index.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import {connect} from 'react-redux';
|
||||
import {updateMetadata} from 'actions/publish';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ publish }) => {
|
||||
return {
|
||||
title: publish.metadata.title,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onMetadataChange: (name, value) => {
|
||||
dispatch(updateMetadata(name, value));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
20
react/containers/PublishTitleInput/view.jsx
Normal file
20
react/containers/PublishTitleInput/view.jsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
class PublishTitleInput extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
}
|
||||
handleInput (e) {
|
||||
const name = e.target.name;
|
||||
const value = e.target.value;
|
||||
this.props.onMetadataChange(name, value);
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<input type="text" id="publish-title" className="input-text text--large input-text--full-width" name="title" placeholder="Give your post a title..." onChange={this.handleInput} value={this.props.title}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PublishTitleInput;
|
12
react/containers/PublishTool/index.js
Normal file
12
react/containers/PublishTool/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import {connect} from 'react-redux';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ publish }) => {
|
||||
return {
|
||||
file : publish.file,
|
||||
status : publish.status.status,
|
||||
message: publish.status.message,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, null)(View);
|
25
react/containers/PublishTool/view.jsx
Normal file
25
react/containers/PublishTool/view.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
import Dropzone from 'containers/Dropzone';
|
||||
import PublishForm from 'containers/PublishForm';
|
||||
import PublishStatus from 'components/PublishStatus';
|
||||
|
||||
class PublishTool extends React.Component {
|
||||
render () {
|
||||
if (this.props.file) {
|
||||
if (this.props.status) {
|
||||
return (
|
||||
<PublishStatus
|
||||
status={this.props.status}
|
||||
message={this.props.message}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <PublishForm />;
|
||||
}
|
||||
} else {
|
||||
return <Dropzone />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default PublishTool;
|
29
react/containers/PublishUrlInput/index.js
Normal file
29
react/containers/PublishUrlInput/index.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {updateClaim, updateError} from 'actions/publish';
|
||||
import {connect} from 'react-redux';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ channel, publish }) => {
|
||||
return {
|
||||
loggedInChannelName : channel.loggedInChannel.name,
|
||||
loggedInChannelShortId: channel.loggedInChannel.shortId,
|
||||
fileName : publish.file.name,
|
||||
publishInChannel : publish.publishInChannel,
|
||||
selectedChannel : publish.selectedChannel,
|
||||
claim : publish.claim,
|
||||
urlError : publish.error.url,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onClaimChange: (value) => {
|
||||
dispatch(updateClaim(value));
|
||||
dispatch(updateError('publishSubmit', null));
|
||||
},
|
||||
onUrlError: (value) => {
|
||||
dispatch(updateError('url', value));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
83
react/containers/PublishUrlInput/view.jsx
Normal file
83
react/containers/PublishUrlInput/view.jsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
import request from 'utils/request';
|
||||
import UrlMiddle from 'components/PublishUrlMiddleDisplay';
|
||||
|
||||
class PublishUrlInput extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.cleanseInput = this.cleanseInput.bind(this);
|
||||
this.setClaimNameFromFileName = this.setClaimNameFromFileName.bind(this);
|
||||
this.checkClaimIsAvailable = this.checkClaimIsAvailable.bind(this);
|
||||
}
|
||||
componentDidMount () {
|
||||
if (!this.props.claim || this.props.claim === '') {
|
||||
this.setClaimNameFromFileName();
|
||||
}
|
||||
}
|
||||
componentWillReceiveProps ({claim: newClaim}) {
|
||||
if (newClaim) {
|
||||
this.checkClaimIsAvailable(newClaim);
|
||||
} else {
|
||||
this.props.onUrlError('Please enter a URL');
|
||||
}
|
||||
}
|
||||
handleInput (event) {
|
||||
let value = event.target.value;
|
||||
value = this.cleanseInput(value);
|
||||
// update the state
|
||||
this.props.onClaimChange(value);
|
||||
}
|
||||
cleanseInput (input) {
|
||||
input = input.replace(/\s+/g, '-'); // replace spaces with dashes
|
||||
input = input.replace(/[^A-Za-z0-9-]/g, ''); // remove all characters that are not A-Z, a-z, 0-9, or '-'
|
||||
return input;
|
||||
}
|
||||
setClaimNameFromFileName () {
|
||||
const fileName = this.props.fileName;
|
||||
const fileNameWithoutEnding = fileName.substring(0, fileName.lastIndexOf('.'));
|
||||
const cleanClaimName = this.cleanseInput(fileNameWithoutEnding);
|
||||
this.props.onClaimChange(cleanClaimName);
|
||||
}
|
||||
checkClaimIsAvailable (claim) {
|
||||
const that = this;
|
||||
request(`/api/claim-is-available/${claim}`)
|
||||
.then(isAvailable => {
|
||||
// console.log('checkClaimIsAvailable request response:', isAvailable);
|
||||
if (isAvailable) {
|
||||
that.props.onUrlError(null);
|
||||
} else {
|
||||
that.props.onUrlError('That url has already been claimed');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
that.props.onUrlError(error.message);
|
||||
});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<p id="input-error-claim-name" className="info-message-placeholder info-message--failure">{this.props.urlError}</p>
|
||||
<div className="column column--3 column--sml-10">
|
||||
<label className="label">URL:</label>
|
||||
</div><div className="column column--7 column--sml-10 input-text--primary span--relative">
|
||||
|
||||
<span className="url-text--secondary">spee.ch / </span>
|
||||
|
||||
<UrlMiddle
|
||||
publishInChannel={this.props.publishInChannel}
|
||||
selectedChannel={this.props.selectedChannel}
|
||||
loggedInChannelName={this.props.loggedInChannelName}
|
||||
loggedInChannelShortId={this.props.loggedInChannelShortId}
|
||||
/>
|
||||
|
||||
<input type="text" id="claim-name-input" className="input-text" name='claim' placeholder="your-url-here" onChange={this.handleInput} value={this.props.claim}/>
|
||||
{ (this.props.claim && !this.props.urlError) && <span id="input-success-claim-name" className="info-message--success span--absolute">{'\u2713'}</span> }
|
||||
{ this.props.urlError && <span id="input-success-channel-name" className="info-message--failure span--absolute">{'\u2716'}</span> }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PublishUrlInput;
|
28
react/reducers/channel.js
Normal file
28
react/reducers/channel.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import * as actions from 'constants/channel_action_types';
|
||||
|
||||
const initialState = {
|
||||
loggedInChannel: {
|
||||
name : null,
|
||||
shortId: null,
|
||||
longId : null,
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
Reducers describe how the application's state changes in response to actions
|
||||
*/
|
||||
|
||||
export default function (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CHANNEL_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
loggedInChannel: {
|
||||
name : action.name,
|
||||
shortId: action.shortId,
|
||||
longId : action.longId,
|
||||
},
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
8
react/reducers/index.js
Normal file
8
react/reducers/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import PublishReducer from 'reducers/publish';
|
||||
import ChannelReducer from 'reducers/channel';
|
||||
|
||||
export default combineReducers({
|
||||
channel: ChannelReducer,
|
||||
publish: PublishReducer,
|
||||
});
|
79
react/reducers/publish.js
Normal file
79
react/reducers/publish.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import * as actions from 'constants/publish_action_types';
|
||||
import { LOGIN } from 'constants/publish_channel_select_states';
|
||||
|
||||
const initialState = {
|
||||
publishInChannel : false,
|
||||
selectedChannel : LOGIN,
|
||||
showMetadataInputs: false,
|
||||
status : {
|
||||
status : null,
|
||||
message: null,
|
||||
},
|
||||
error: {
|
||||
file : null,
|
||||
url : null,
|
||||
channel : null,
|
||||
publishSubmit: null,
|
||||
},
|
||||
file : null,
|
||||
claim : '',
|
||||
metadata: {
|
||||
title : '',
|
||||
thumbnail : '',
|
||||
description: '',
|
||||
license : '',
|
||||
nsfw : false,
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
Reducers describe how the application's state changes in response to actions
|
||||
*/
|
||||
|
||||
export default function (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.FILE_SELECTED:
|
||||
return Object.assign({}, state, {
|
||||
file: action.file,
|
||||
});
|
||||
case actions.FILE_CLEAR:
|
||||
return initialState;
|
||||
case actions.METADATA_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
metadata: Object.assign({}, state.metadata, {
|
||||
[action.name]: action.value,
|
||||
}),
|
||||
});
|
||||
case actions.CLAIM_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
claim: action.value,
|
||||
});
|
||||
case actions.SET_PUBLISH_IN_CHANNEL:
|
||||
return Object.assign({}, state, {
|
||||
publishInChannel: action.channel,
|
||||
});
|
||||
case actions.PUBLISH_STATUS_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
status: Object.assign({}, state.status, {
|
||||
status : action.status,
|
||||
message: action.message,
|
||||
}),
|
||||
});
|
||||
case actions.ERROR_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
error: Object.assign({}, state.error, {
|
||||
[action.name]: action.value,
|
||||
}),
|
||||
});
|
||||
case actions.SELECTED_CHANNEL_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
selectedChannel: action.value,
|
||||
});
|
||||
case actions.TOGGLE_METADATA_INPUTS:
|
||||
return Object.assign({}, state, {
|
||||
showMetadataInputs: action.value,
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
38
react/utils/file.js
Normal file
38
react/utils/file.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
module.exports = {
|
||||
validateFile (file) {
|
||||
if (!file) {
|
||||
console.log('no file found');
|
||||
throw new Error('no file provided');
|
||||
}
|
||||
if (/'/.test(file.name)) {
|
||||
console.log('file name had apostrophe in it');
|
||||
throw new Error('apostrophes are not allowed in the file name');
|
||||
}
|
||||
// validate size and type
|
||||
switch (file.type) {
|
||||
case 'image/jpeg':
|
||||
case 'image/jpg':
|
||||
case 'image/png':
|
||||
if (file.size > 10000000) {
|
||||
console.log('file was too big');
|
||||
throw new Error('Sorry, images are limited to 10 megabytes.');
|
||||
}
|
||||
break;
|
||||
case 'image/gif':
|
||||
if (file.size > 50000000) {
|
||||
console.log('file was too big');
|
||||
throw new Error('Sorry, GIFs are limited to 50 megabytes.');
|
||||
}
|
||||
break;
|
||||
case 'video/mp4':
|
||||
if (file.size > 50000000) {
|
||||
console.log('file was too big');
|
||||
throw new Error('Sorry, videos are limited to 50 megabytes.');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log('file type is not supported');
|
||||
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
|
||||
}
|
||||
},
|
||||
}
|
44
react/utils/request.js
Normal file
44
react/utils/request.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Parses the JSON returned by a network request
|
||||
*
|
||||
* @param {object} response A response from a network request
|
||||
*
|
||||
* @return {object} The parsed JSON from the request
|
||||
*/
|
||||
function parseJSON (response) {
|
||||
if (response.status === 204 || response.status === 205) {
|
||||
return null;
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a network request came back fine, and throws an error if not
|
||||
*
|
||||
* @param {object} response A response from a network request
|
||||
*
|
||||
* @return {object|undefined} Returns either the response, or throws an error
|
||||
*/
|
||||
function checkStatus (response) {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const error = new Error(response.statusText);
|
||||
error.response = response;
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a URL, returning a promise
|
||||
*
|
||||
* @param {string} url The URL we want to request
|
||||
* @param {object} [options] The options we want to pass to "fetch"
|
||||
*
|
||||
* @return {object} The response data
|
||||
*/
|
||||
export default function request (url, options) {
|
||||
return fetch(url, options)
|
||||
.then(checkStatus)
|
||||
.then(parseJSON);
|
||||
}
|
|
@ -71,7 +71,6 @@ module.exports = (app) => {
|
|||
if (result === true) {
|
||||
res.status(200).json(true);
|
||||
} else {
|
||||
// logger.debug(`Rejecting '${params.name}' because that name has already been claimed by this site`);
|
||||
res.status(200).json(false);
|
||||
}
|
||||
})
|
||||
|
@ -86,7 +85,6 @@ module.exports = (app) => {
|
|||
if (result === true) {
|
||||
res.status(200).json(true);
|
||||
} else {
|
||||
// logger.debug(`Rejecting '${params.name}' because that channel has already been claimed`);
|
||||
res.status(200).json(false);
|
||||
}
|
||||
})
|
||||
|
|
|
@ -13,14 +13,38 @@ module.exports = (app) => {
|
|||
});
|
||||
});
|
||||
// route for log in
|
||||
app.post('/login', passport.authenticate('local-login'), (req, res) => {
|
||||
// logger.debug('req.user:', req.user); // req.user contains the authenticated user's info
|
||||
logger.debug('successful login');
|
||||
res.status(200).json({
|
||||
success : true,
|
||||
channelName : req.user.channelName,
|
||||
channelClaimId: req.user.channelClaimId,
|
||||
shortChannelId: req.user.shortChannelId,
|
||||
});
|
||||
app.post('/login', (req, res, next) => {
|
||||
passport.authenticate('local-login', (err, user, info) => {
|
||||
logger.debug('info:', info);
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (!user) {
|
||||
return res.status(200).json({
|
||||
success: false,
|
||||
message: info.message,
|
||||
});
|
||||
}
|
||||
logger.debug('successful login');
|
||||
req.logIn(user, (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
return res.status(200).json({
|
||||
success : true,
|
||||
channelName : req.user.channelName,
|
||||
channelClaimId: req.user.channelClaimId,
|
||||
shortChannelId: req.user.shortChannelId,
|
||||
});
|
||||
});
|
||||
})(req, res, next);
|
||||
});
|
||||
// see if user is authenticated, and return credentials if so
|
||||
app.get('/user', (req, res) => {
|
||||
if (req.user) {
|
||||
res.status(200).json({success: true, message: req.user});
|
||||
} else {
|
||||
res.status(200).json({success: false, message: 'user is not logged in'});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--padded">
|
||||
<div class="column column--5 column--med-10 align-content-top">
|
||||
<div class="column column--8 column--med-10">
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--padded">
|
||||
<div class="row">
|
||||
{{#ifConditional this.totalPages '===' 0}}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--padded">
|
||||
<h3>404: Not Found</h3>
|
||||
<p>That page does not exist. Return <a class="link--primary" href="/">home</a>.</p>
|
||||
|
|
|
@ -1,52 +1,11 @@
|
|||
<div id="react-nav-bar"></div>
|
||||
<div class="row row--tall flex-container--column">
|
||||
<form>
|
||||
<input class="input-file" type="file" id="file_input" name="file_input" accept="video/*,image/*" onchange="publishFileFunctions.previewAndStageFile(event.target.files[0])" enctype="multipart/form-data"/>
|
||||
</form>
|
||||
<div id="primary-dropzone" class="dropzone row row--margined row--padded row--tall flex-container--column flex-container--center-center" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" ondragend="dragend_handler(event)" ondragenter="dragenter_handler(event)" ondragleave="dragexit_handler(event)" onclick="publishFileFunctions.triggerFileChooser('file_input', event)">
|
||||
<div id="primary-dropzone-instructions">
|
||||
<p class="info-message-placeholder info-message--failure" id="input-error-file-selection" hidden="true"></p>
|
||||
<p>Drag & drop image or video here to publish</p>
|
||||
<p class="fine-print">OR</p>
|
||||
<p class="blue--underlined">CHOOSE FILE</p>
|
||||
</div>
|
||||
<div id="dropbzone-dragover" class="hidden">
|
||||
<p class="blue">Drop it.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="publish-form" class="hidden">
|
||||
<div class="row row--padded row--no-bottom">
|
||||
<div class="column column--10">
|
||||
<!-- title input -->
|
||||
<input type="text" id="publish-title" class="input-text text--large input-text--full-width" placeholder="Give your post a title...">
|
||||
</div>
|
||||
<div class="column column--5 column--sml-10" >
|
||||
<!-- preview -->
|
||||
<div class="row row--padded">
|
||||
<div id="asset-preview-holder" class="dropzone" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" ondragend="dragend_handler(event)" ondragenter="preview_onmouseenter_handler()" ondragleave="preview_onmouseleave_handler()" onmouseenter="preview_onmouseenter_handler()" onmouseleave="preview_onmouseleave_handler()" onclick="publishFileFunctions.triggerFileChooser('file_input', event)">
|
||||
<div id="asset-preview-dropzone-instructions" class="hidden">
|
||||
<p>Drag & drop image or video here</p>
|
||||
<p class="fine-print">OR</p>
|
||||
<p class="blue--underlined">CHOOSE FILE</p>
|
||||
</div>
|
||||
<div id="asset-preview-target"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><div class="column column--5 column--sml-10 align-content-top">
|
||||
<div id="publish-active-area" class="row row--padded">
|
||||
{{> publishForm-Channel}}
|
||||
{{> publishForm-Url}}
|
||||
{{> publishForm-Thumbnail}}
|
||||
{{> publishForm-Details}}
|
||||
{{> publishForm-Submit}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="publish-status" class="hidden">
|
||||
<div class="row row--margined">
|
||||
<div id="publish-update" class="row align-content-center"></div>
|
||||
<div id="publish-progress-bar" class="row align-content-center"></div>
|
||||
<div id="upload-percent" class="row align-content-center"></div>
|
||||
<div id="react-publish-tool" class="row row--padded row--tall flex-container--column">
|
||||
<div class="row row--padded row--tall flex-container--column flex-container--center-center">
|
||||
<p>loading...</p>
|
||||
{{> progressBar}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/bundle/bundle.js"></script>
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
</head>
|
||||
<body id="channel-body">
|
||||
<script src="/assets/js/generalFunctions.js"></script>
|
||||
<script src="/assets/js/navBarFunctions.js"></script>
|
||||
{{> navBar}}
|
||||
{{{ body }}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -18,13 +18,6 @@
|
|||
<body id="main-body">
|
||||
<script src="/assets/js/generalFunctions.js"></script>
|
||||
<script src="/assets/js/validationFunctions.js"></script>
|
||||
<script src="/assets/js/publishFileFunctions.js"></script>
|
||||
<script src="/assets/js/authFunctions.js"></script>
|
||||
<script src="/assets/js/loginFunctions.js"></script>
|
||||
<script src="/assets/js/dropzoneFunctions.js"></script>
|
||||
<script src="/assets/js/createChannelFunctions.js"></script>
|
||||
<script src="/assets/js/navBarFunctions.js"></script>
|
||||
{{> navBar}}
|
||||
{{{ body }}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -14,9 +14,7 @@
|
|||
</head>
|
||||
<body id="show-body">
|
||||
<script src="/assets/js/generalFunctions.js"></script>
|
||||
<script src="/assets/js/navBarFunctions.js"></script>
|
||||
<script src="/assets/js/assetConstructor.js"></script>
|
||||
{{> navBar}}
|
||||
{{{ body }}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--padded">
|
||||
<div class="column column--5 column--med-10 align-content-top">
|
||||
<div class="column column--8 column--med-10">
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--padded">
|
||||
<h3>No Channel</h3>
|
||||
<p>There are no published channels matching your url</p>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--padded">
|
||||
<h3>No Claims</h3>
|
||||
<p>There are no free assets at that claim. You should publish one at <a class="link--primary" href="/">spee.ch</a>.</p>
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
</div>
|
||||
|
||||
<div id="show-share-buttons">
|
||||
<div class="row row--padded row--wide">
|
||||
<div class="row row--padded row--no-bottom row--wide">
|
||||
<div class="column column--2 column--med-10">
|
||||
<span class="text">Share:</span>
|
||||
</div><div class="column column--7 column--med-10">
|
||||
|
@ -64,14 +64,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row--wide">
|
||||
<a class="text link--primary" id="show-details-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-openlabel="[less]" data-closedlabel="[more]" data-slaveelementid="show-details">[more]</a>
|
||||
</div>
|
||||
|
||||
<div id="show-details" class="row row--padded row--wide" hidden="true">
|
||||
<div id="show-details" class="row row--padded row--no-bottom row--wide" hidden="true">
|
||||
<div id="show-claim-name">
|
||||
<div class="column column--2 column--med-10">
|
||||
<span class="text">Name:</span>
|
||||
<span class="text">Claim Name:</span>
|
||||
</div><div class="column column--8 column--med-10">
|
||||
{{claimInfo.name}}
|
||||
</div>
|
||||
|
@ -83,13 +79,6 @@
|
|||
{{claimInfo.claimId}}
|
||||
</div>
|
||||
</div>
|
||||
<div id="show-claim-id">
|
||||
<div class="column column--2 column--med-10">
|
||||
<span class="text">File Name:</span>
|
||||
</div><div class="column column--8 column--med-10">
|
||||
{{claimInfo.fileName}}
|
||||
</div>
|
||||
</div>
|
||||
<div id="show-claim-id">
|
||||
<div class="column column--2 column--med-10">
|
||||
<span class="text">File Type:</span>
|
||||
|
@ -102,3 +91,37 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row--padded row--no-bottom row--wide">
|
||||
<a class="text link--primary" id="show-details-toggle" href="#" onclick="toggleSection(event)" data-status="closed">[more]</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleSection(event){
|
||||
event.preventDefault();
|
||||
var dataSet = event.target.dataset;
|
||||
var status = dataSet.status;
|
||||
var toggle = document.getElementById("show-details-toggle");
|
||||
var details = document.getElementById("show-details");
|
||||
if (status === "closed") {
|
||||
details.hidden = false;
|
||||
toggle.innerText = "[less]";
|
||||
toggle.dataset.status = "open";
|
||||
} else {
|
||||
details.hidden = true;
|
||||
toggle.innerText = "[more]";
|
||||
toggle.dataset.status = "closed";
|
||||
}
|
||||
}
|
||||
function copyToClipboard(event){
|
||||
var elementToCopy = event.target.dataset.elementtocopy;
|
||||
var element = document.getElementById(elementToCopy);
|
||||
var errorElement = 'input-error-copy-text' + elementToCopy;
|
||||
element.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
validationFunctions.showError(errorElement, 'Oops, unable to copy');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -29,9 +29,11 @@
|
|||
|
||||
<div id="channel-publish-in-progress" hidden="true">
|
||||
<p>Creating your new channel. This may take a few seconds...</p>
|
||||
<div id="create-channel-progress-bar"></div>
|
||||
{{> progressBar}}
|
||||
</div>
|
||||
|
||||
<div id="channel-publish-done" hidden="true">
|
||||
<p>Your channel has been successfully created!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/createChannelFunctions.js"></script>
|
||||
|
|
|
@ -23,4 +23,6 @@
|
|||
<div class="row row--wide">
|
||||
<button class="button--primary" onclick="loginToChannel(event)">Authenticate</button>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
|
||||
<script src="/assets/js/loginFunctions.js"></script>
|
||||
|
|
|
@ -24,15 +24,18 @@
|
|||
<span class="nav-bar-tagline">Open-source, decentralized image and video sharing.</span>
|
||||
</div>
|
||||
<div class="nav-bar--right">
|
||||
<a class="nav-bar-link link--nav" href="/">Upload</a>
|
||||
<a class="nav-bar-link link--nav" href="/popular">Popular</a>
|
||||
<a class="nav-bar-link link--nav" href="/">Publish</a>
|
||||
<!--<a class="nav-bar-link link--nav" href="/popular">Popular</a>-->
|
||||
<a class="nav-bar-link link--nav" href="/about">About</a>
|
||||
<select type="text" id="nav-bar-channel-select" class="select select--arrow link--nav" onchange="toggleNavBarSelection(event.target.selectedOptions[0].value)" {{#unless user}}style="display:none"{{/unless}}>
|
||||
<option id="nav-bar-channel-select-channel-option">@{{user.userName}}</option>
|
||||
<option value="VIEW">View</option>
|
||||
<option value="LOGOUT">Logout</option>
|
||||
</select>
|
||||
<a id="nav-bar-login-link" class="nav-bar-link link--nav" href="/login" {{#if user}}style="display:none"{{/if}}>Channel</a>
|
||||
{{#if user }}
|
||||
<select type="text" id="nav-bar-channel-select" class="select select--arrow link--nav" onchange="toggleNavBarSelection(event)">
|
||||
<option id="nav-bar-channel-select-channel-option" >{{user.channelName}}</option>
|
||||
<option value="VIEW" data-channelUrl="/{{user.channelName}}:{{user.channelClaimId}}">View</option>
|
||||
<option value="LOGOUT">Logout</option>
|
||||
</select>
|
||||
{{else}}
|
||||
<a id="nav-bar-login-link" class="nav-bar-link link--nav" href="/login">Channel</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -48,4 +51,18 @@
|
|||
link.setAttribute('class', 'select select--arrow link--nav-active');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
// function to send user to their channel if selected
|
||||
function toggleNavBarSelection (event) {
|
||||
console.log('toggleNavBarSelection event', event);
|
||||
const selectedOption = event.target.selectedOptions[0].value;
|
||||
if (selectedOption === 'LOGOUT') {
|
||||
// send logout request to server
|
||||
window.location.href = '/logout';
|
||||
} else if (selectedOption === 'VIEW') {
|
||||
// redirect to channel page
|
||||
const channelUrl = event.target.selectedOptions[0].dataset.channelurl;
|
||||
console.log('url:', channelUrl);
|
||||
window.location.href = channelUrl;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
<!-- select whether to publish anonymously or in a channel -->
|
||||
<div class="row row--padded row--short row--wide">
|
||||
<div class="column column--10">
|
||||
<form>
|
||||
<div class="column column--3 column--med-10">
|
||||
<input type="radio" name="anonymous-or-channel" id="anonymous-radio" class="input-radio" value="anonymous" {{#unless user}}checked {{/unless}} onchange="toggleChannel(event.target.value)"/>
|
||||
<label class="label label--pointer" for="anonymous-radio">Anonymous</label>
|
||||
</div><div class="column column--7 column--med-10">
|
||||
<input type="radio" name="anonymous-or-channel" id="channel-radio" class="input-radio" value="in a channel" {{#if user}}checked {{/if}} onchange="toggleChannel(event.target.value)"/>
|
||||
<label class="label label--pointer" for="channel-radio">In a channel</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="channel-select-options" {{#unless user}}hidden="true"{{/unless}}>
|
||||
<div class="row row--padded row--no-top row--no-bottom row--wide">
|
||||
<!--error display-->
|
||||
<p id="input-error-channel-select" class="info-message-placeholder info-message--failure"></p>
|
||||
<!--channel login/create select-->
|
||||
<div class="column column--3">
|
||||
<label class="label" for="channel-name-select">Channel:</label>
|
||||
</div><div class="column column--7">
|
||||
<select type="text" id="channel-name-select" class="select select--arrow" onchange="toggleSelectedChannel(event.target.selectedOptions[0].value)">
|
||||
{{#if user}}
|
||||
<option value="{{user.channelName}}" id="publish-channel-select-channel-option">{{user.channelName}}</option>
|
||||
{{/if}}
|
||||
<option value="login">Existing</option>
|
||||
<option value="new" >New</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- log into an existing channel -->
|
||||
<div id="channel-login-details" class="row row--padded row--short row--wide" {{#if user}}hidden="true"{{/if}}>
|
||||
{{> channelLoginForm}}
|
||||
</div>
|
||||
<!-- create a channel -->
|
||||
<div id="channel-create-details" class="row row--padded row--short row--wide" hidden="true">
|
||||
{{> channelCreationForm}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
// show or hide the channel selection tools
|
||||
function toggleChannel (selectedOption) {
|
||||
const channelSelectOptions = document.getElementById('channel-select-options');
|
||||
// show/hide the login and new channel forms
|
||||
if (selectedOption === 'anonymous') {
|
||||
channelSelectOptions.hidden = true;
|
||||
channelSelectOptions.hidden = true;
|
||||
// update url
|
||||
updateUrl(selectedOption);
|
||||
} else if (selectedOption === 'in a channel') {
|
||||
channelSelectOptions.hidden = false;
|
||||
// update url
|
||||
let selectedChannel = document.getElementById('channel-name-select').selectedOptions[0].value
|
||||
toggleSelectedChannel(selectedChannel);
|
||||
} else {
|
||||
console.log('selected option was not recognized');
|
||||
}
|
||||
|
||||
}
|
||||
// show or hide the channel create/login tool
|
||||
function toggleSelectedChannel (selectedChannel) {
|
||||
const createChannelTool = document.getElementById('channel-create-details');
|
||||
const loginToChannelTool = document.getElementById('channel-login-details');
|
||||
// show/hide the login and new channel forms
|
||||
if (selectedChannel === 'new') {
|
||||
createChannelTool.hidden = false;
|
||||
loginToChannelTool.hidden = true;
|
||||
} else if (selectedChannel === 'login') {
|
||||
loginToChannelTool.hidden = false;
|
||||
createChannelTool.hidden = true;
|
||||
} else {
|
||||
// hide the login and new channel forms
|
||||
loginToChannelTool.hidden = true;
|
||||
createChannelTool.hidden = true;
|
||||
validationFunctions.hideError(document.getElementById('input-error-channel-select'));
|
||||
}
|
||||
// update url
|
||||
updateUrl(selectedChannel);
|
||||
}
|
||||
function updateUrl (selectedOption) {
|
||||
const urlChannel = document.getElementById('url-channel');
|
||||
const urlNoChannelPlaceholder = document.getElementById('url-no-channel-placeholder');
|
||||
const urlChannelPlaceholder = document.getElementById('url-channel-placeholder');
|
||||
if (selectedOption === 'new' || selectedOption === 'login' || selectedOption === ''){
|
||||
urlChannel.hidden = true;
|
||||
urlNoChannelPlaceholder.hidden = true;
|
||||
urlChannelPlaceholder.hidden = false;
|
||||
} else if (selectedOption === 'anonymous'){
|
||||
urlChannel.hidden = true;
|
||||
urlNoChannelPlaceholder.hidden = false;
|
||||
urlChannelPlaceholder.hidden = true;
|
||||
} else {
|
||||
urlChannel.hidden = false;
|
||||
// show channel and short id
|
||||
const selectedChannel = getCookie('channel_name');
|
||||
const shortChannelId = getCookie('short_channel_id');
|
||||
urlChannel.innerText = `${selectedChannel}:${shortChannelId}`;
|
||||
urlNoChannelPlaceholder.hidden = true;
|
||||
urlChannelPlaceholder.hidden = true;
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,50 +0,0 @@
|
|||
<div class="row row--padded row--no-top row--no-bottom row--wide">
|
||||
<div class="column column--10">
|
||||
<a class="label link--primary" id="publish-details-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-openlabel="[less]" data-closedlabel="[more]" data-slaveelementid="publish-details">[more]</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="publish-details" hidden="true" class="row row--padded row--wide">
|
||||
|
||||
<!-- description input -->
|
||||
<div class="row row--no-top">
|
||||
<div class="column column--3 column--med-10 align-content-top">
|
||||
<label for="publish-license" class="label">Description:</label>
|
||||
</div><div class="column column--7 column--sml-10">
|
||||
<textarea rows="1" id="publish-description" class="textarea textarea--primary textarea--full-width" placeholder="Optional description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row--no-top">
|
||||
<div class="column column--3 column--med-10">
|
||||
<label for="publish-license" class="label">License:</label>
|
||||
</div><div class="column column--7 column--sml-10">
|
||||
<select type="text" id="publish-license" class="select select--primary">
|
||||
<option value=" ">Unspecified</option>
|
||||
<option value="Public Domain">Public Domain</option>
|
||||
<option value="Creative Commons">Creative Commons</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row--no-top">
|
||||
<div class="column column--3">
|
||||
<label for="publish-nsfw" class="label">Mature:</label>
|
||||
</div><div class="column column--7">
|
||||
<input class="input-checkbox" type="checkbox" id="publish-nsfw">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
const textarea = document.getElementById('publish-description');
|
||||
const limit = 200;
|
||||
textarea.oninput = () => {
|
||||
textarea.style.height = '';
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, limit) + 'px';
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<div class="row row--padded row--wide">
|
||||
<div class="input-error" id="input-error-publish-submit" hidden="true"></div>
|
||||
<button id="publish-submit" class="button--primary button--large" onclick="publishFileFunctions.publishStagedFile(event)">Upload</button>
|
||||
</div>
|
||||
<div class="row row--short align-content-center">
|
||||
<button class="button--cancel" onclick="publishFileFunctions.cancelPublish()">Cancel</button>
|
||||
</div>
|
||||
<div class="row row--short align-content-center">
|
||||
<p class="fine-print">By clicking 'Upload', you affirm that you have the rights to publish this content to the LBRY network, and that you understand the properties of publishing it to a decentralized, user-controlled network. <a class="link--primary" target="_blank" href="https://lbry.io/learn">Read more.</a></p>
|
||||
</div>
|
|
@ -1,56 +0,0 @@
|
|||
<div class="row row--padded row--wide row--no-top" id="publish-thumbnail" hidden="true">
|
||||
<div class="column column--3 column--sml-10">
|
||||
<label class="label">Thumbnail:</label>
|
||||
</div><div class="column column--6 column--sml-10">
|
||||
<div class="input-text--primary">
|
||||
<input type="text" id="claim-thumbnail-input" class="input-text input-text--full-width" placeholder="https://spee.ch/xyz/example.jpg" value="" oninput="updateVideoThumb(event)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function urlIsAnImage(url) {
|
||||
return(url.match(/\.(jpeg|jpg|gif|png)$/) != null);
|
||||
}
|
||||
|
||||
function testImage(url, timeoutT) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var timeout = timeoutT || 5000;
|
||||
var timer, img = new Image();
|
||||
img.onerror = img.onabort = function () {
|
||||
clearTimeout(timer);
|
||||
reject("error");
|
||||
};
|
||||
img.onload = function () {
|
||||
clearTimeout(timer);
|
||||
resolve("success");
|
||||
};
|
||||
timer = setTimeout(function () {
|
||||
// reset .src to invalid URL so it stops previous
|
||||
// loading, but doesn't trigger new load
|
||||
img.src = "//!!!!/test.jpg";
|
||||
reject("timeout");
|
||||
}, timeout);
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
function updateVideoThumb(event){
|
||||
var videoPreview = document.getElementById('asset-preview');
|
||||
var imageUrl = event.target.value;
|
||||
if (urlIsAnImage(imageUrl)){
|
||||
testImage(imageUrl, 3000)
|
||||
.then(function(result) {
|
||||
if (result === 'success'){
|
||||
videoPreview.src = imageUrl;
|
||||
} else if (result === 'timeout') {
|
||||
console.log('could not resolve the provided thumbnail image url');
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.log('encountered an error loading thumbnail image url.')
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,16 +0,0 @@
|
|||
<div class="row row--padded row--wide">
|
||||
<!--error display-->
|
||||
<p id="input-error-claim-name" class="info-message-placeholder info-message--failure" hidden="true"></p>
|
||||
<!--url selection-->
|
||||
<div class="column column--3 column--sml-10">
|
||||
<label class="label">URL:</label>
|
||||
</div><div class="column column--7 column--sml-10 input-text--primary span--relative">
|
||||
<span class="url-text--secondary">spee.ch /</span>
|
||||
<span id="url-channel" class="url-text--secondary" {{#if user}}{{else}}hidden="true"{{/if}}>{{user.channelName}}:{{user.shortChannelId}}</span>
|
||||
<span id="url-no-channel-placeholder" class="url-text--secondary tooltip" {{#if user}}hidden="true"{{else}}{{/if}}>xyz<span class="tooltip-text">This will be a random id</span></span>
|
||||
<span id="url-channel-placeholder" class="url-text--secondary tooltip" hidden="true">@channel<span class="tooltip-text">Select a channel above</span></span> /
|
||||
<input type="text" id="claim-name-input" class="input-text" placeholder="your-url-here" oninput="validationFunctions.checkClaimName(event.target.value)">
|
||||
<span id="input-success-claim-name" class="info-message--success span--absolute"></span>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -1,3 +1,4 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--padded">
|
||||
<div class="grid">
|
||||
{{#each trendingAssets}}
|
||||
|
@ -20,4 +21,4 @@
|
|||
percentPosition: true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--padded">
|
||||
<h3>Error</h3>
|
||||
<p>Unfortnately, Spee.ch encountered an error. You can help us out, by reporting the below error message in the #speech channel on <a class="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">LBRY Discord</a>!</p>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--tall row--padded">
|
||||
<div class="column column--10">
|
||||
<!-- title -->
|
||||
|
@ -14,4 +15,4 @@
|
|||
{{> assetInfo}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
32
webpack.config.js
Normal file
32
webpack.config.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
const Path = require('path');
|
||||
|
||||
const REACT_ROOT = Path.resolve(__dirname, 'react/');
|
||||
|
||||
module.exports = {
|
||||
entry : ['whatwg-fetch', './react/app.js'],
|
||||
output: {
|
||||
path : Path.join(__dirname, '/public/bundle/'),
|
||||
filename: 'bundle.js',
|
||||
},
|
||||
watch : true,
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test : /.jsx?$/,
|
||||
loader : 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
query : {
|
||||
presets: ['es2015', 'react', 'stage-2'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
modules: [
|
||||
REACT_ROOT,
|
||||
'node_modules',
|
||||
__dirname,
|
||||
],
|
||||
extensions: ['.js', '.jsx', '.scss'],
|
||||
},
|
||||
};
|
Loading…
Reference in a new issue