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",
|
"start": "node speech.js",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"fix": "eslint . --fix",
|
"fix": "eslint . --fix",
|
||||||
"precommit": "eslint ."
|
"precommit": "eslint .",
|
||||||
|
"babel": "babel",
|
||||||
|
"webpack": "webpack"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -41,16 +43,27 @@
|
||||||
"nodemon": "^1.11.0",
|
"nodemon": "^1.11.0",
|
||||||
"passport": "^0.4.0",
|
"passport": "^0.4.0",
|
||||||
"passport-local": "^1.0.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": "^2.83.0",
|
||||||
"request-promise": "^4.2.2",
|
"request-promise": "^4.2.2",
|
||||||
"sequelize": "^4.1.0",
|
"sequelize": "^4.1.0",
|
||||||
"sequelize-cli": "^3.0.0-3",
|
"sequelize-cli": "^3.0.0-3",
|
||||||
"sleep": "^5.1.1",
|
"sleep": "^5.1.1",
|
||||||
"universal-analytics": "^0.4.13",
|
"universal-analytics": "^0.4.13",
|
||||||
|
"whatwg-fetch": "^2.0.3",
|
||||||
"winston": "^2.3.1",
|
"winston": "^2.3.1",
|
||||||
"winston-slack-webhook": "billbitt/winston-slack-webhook"
|
"winston-slack-webhook": "billbitt/winston-slack-webhook"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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": "^4.1.2",
|
||||||
"chai-http": "^3.0.0",
|
"chai-http": "^3.0.0",
|
||||||
"eslint": "3.19.0",
|
"eslint": "3.19.0",
|
||||||
|
@ -61,6 +74,8 @@
|
||||||
"eslint-plugin-react": "6.10.3",
|
"eslint-plugin-react": "6.10.3",
|
||||||
"eslint-plugin-standard": "3.0.1",
|
"eslint-plugin-standard": "3.0.1",
|
||||||
"husky": "^0.13.4",
|
"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 => {
|
.then(user => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// logger.debug('no user found');
|
// 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) => {
|
user.comparePassword(password, (passwordErr, isMatch) => {
|
||||||
if (passwordErr) {
|
if (passwordErr) {
|
||||||
|
@ -46,7 +46,7 @@ module.exports = new PassportLocalStrategy(
|
||||||
}
|
}
|
||||||
if (!isMatch) {
|
if (!isMatch) {
|
||||||
// logger.debug('incorrect password');
|
// 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');
|
logger.debug('Password was a match, returning User');
|
||||||
return returnUserAndChannelInfo(user)
|
return returnUserAndChannelInfo(user)
|
||||||
|
|
|
@ -483,32 +483,31 @@ table {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
#primary-dropzone-instructions, #dropbzone-dragover {
|
#dropzone-text-holder {
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.position-absolute {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
left: 0;
|
||||||
left: 0px;
|
top: 0;
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#asset-preview-holder {
|
#dropzone-dragover, #dropzone-instructions {
|
||||||
position: relative;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#asset-preview {
|
#asset-preview {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0.5rem;
|
width: 100%;
|
||||||
width: calc(100% - 1rem);
|
}
|
||||||
|
|
||||||
|
.dim {
|
||||||
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Assets */
|
/* Assets */
|
||||||
|
|
||||||
.asset {
|
.asset {
|
||||||
max-width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#show-body #asset-boilerpate {
|
#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 () {
|
function showChannelCreateInProgressDisplay () {
|
||||||
const publishChannelForm = document.getElementById('publish-channel-form');
|
const publishChannelForm = document.getElementById('publish-channel-form');
|
||||||
const inProgress = document.getElementById('channel-publish-in-progress');
|
const inProgress = document.getElementById('channel-publish-in-progress');
|
||||||
const channelProgressBar = document.getElementById('create-channel-progress-bar');
|
|
||||||
publishChannelForm.hidden = true;
|
publishChannelForm.hidden = true;
|
||||||
inProgress.hidden = false;
|
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() {
|
function showChannelCreateDoneDisplay() {
|
||||||
const inProgress = document.getElementById('channel-publish-in-progress');
|
const inProgress = document.getElementById('channel-publish-in-progress');
|
||||||
inProgress.hidden=true;
|
inProgress.hidden=true;
|
||||||
|
@ -22,27 +20,38 @@ function showChannelCreationError(msg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function publishNewChannel (event) {
|
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;
|
const password = document.getElementById('new-channel-password').value;
|
||||||
// prevent default so this script can handle submission
|
// prevent default so this script can handle submission
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// validate submission
|
// validate submission
|
||||||
validationFunctions.validateNewChannelSubmission(userName, password)
|
validationFunctions.validateNewChannelSubmission(username, password)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
showChannelCreateInProgressDisplay();
|
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 => {
|
.then(signupResult => {
|
||||||
setUserCookies(result.channelName, result.channelClaimId, result.shortChannelId);
|
console.log('signup success:', signupResult);
|
||||||
showChannelCreateDoneDisplay();
|
showChannelCreateDoneDisplay();
|
||||||
// if user is on the home page, update the needed elements without reloading
|
window.location = '/';
|
||||||
if (window.location.pathname === '/') {
|
|
||||||
replaceChannelOptionInPublishChannelSelect(result.channelName);
|
|
||||||
replaceChannelOptionInNavBarChannelSelect(result.channelName);
|
|
||||||
// if user is not on home page, redirect to home page
|
|
||||||
} else {
|
|
||||||
window.location = '/';
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error.name === 'ChannelNameError' || error.name === 'ChannelPasswordError'){
|
if (error.name === 'ChannelNameError' || error.name === 'ChannelPasswordError'){
|
||||||
|
@ -50,7 +59,7 @@ function publishNewChannel (event) {
|
||||||
validationFunctions.showError(channelNameErrorDisplayElement, error.message);
|
validationFunctions.showError(channelNameErrorDisplayElement, error.message);
|
||||||
} else {
|
} else {
|
||||||
console.log('signup failure:', error);
|
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
|
// Create new error objects, that prototypically inherit from the Error constructor
|
||||||
function FileError(message) {
|
function FileError(message) {
|
||||||
this.name = 'FileError';
|
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) {
|
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;
|
const password = document.getElementById('channel-login-password-input').value;
|
||||||
// prevent default
|
// prevent default
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
validationFunctions.validateNewChannelLogin(userName, password)
|
validationFunctions.validateNewChannelLogin(username, password)
|
||||||
.then(() => {
|
.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 => {
|
.then(function({success, message}) {
|
||||||
setUserCookies(result.channelName, result.channelClaimId, result.shortChannelId);
|
if (success) {
|
||||||
// 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 = '/';
|
||||||
|
} else {
|
||||||
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
const loginErrorDisplayElement = document.getElementById('login-error-display-element');
|
const loginErrorDisplayElement = document.getElementById('login-error-display-element');
|
||||||
if (error.name){
|
if (error.message){
|
||||||
validationFunctions.showError(loginErrorDisplayElement, error.message);
|
validationFunctions.showError(loginErrorDisplayElement, error.message);
|
||||||
} else {
|
} else {
|
||||||
validationFunctions.showError(loginErrorDisplayElement, 'There was an error logging into your channel');
|
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
|
// validation function which checks the proposed file's type, size, and name
|
||||||
const validationFunctions = {
|
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) {
|
validateChannelName: function (name) {
|
||||||
name = name.substring(name.indexOf('@') + 1);
|
name = name.substring(name.indexOf('@') + 1);
|
||||||
// ensure a name was entered
|
// ensure a name was entered
|
||||||
|
@ -65,15 +17,21 @@ const validationFunctions = {
|
||||||
throw new ChannelPasswordError("You must enter a password for you channel");
|
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
|
// 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) {
|
isNameAvailable: function (name, apiUrl) {
|
||||||
|
console.log('isNameAvailable?', name);
|
||||||
const url = apiUrl + 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) {
|
showError: function (errorDisplay, errorMsg) {
|
||||||
errorDisplay.hidden = false;
|
errorDisplay.hidden = false;
|
||||||
|
@ -91,90 +49,35 @@ const validationFunctions = {
|
||||||
successElement.hidden = true;
|
successElement.hidden = true;
|
||||||
successElement.innerHTML = "";
|
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;
|
var that = this;
|
||||||
try {
|
try {
|
||||||
// check to make sure the characters are valid
|
// check to make sure the characters are valid
|
||||||
validateName(name);
|
that.validateChannelName(channelName);
|
||||||
// check to make sure it is available
|
// check to make sure it is available
|
||||||
that.isNameAvailable(name, apiUrl)
|
that.isChannelNameAvailable(channelName)
|
||||||
.then(function (result) {
|
.then(function(isAvailable){
|
||||||
if (result === true) {
|
console.log('isChannelNameAvailable:', isAvailable);
|
||||||
that.hideError(errorDisplayElement);
|
if (isAvailable) {
|
||||||
that.showSuccess(successDisplayElement)
|
that.hideError(errorDisplayElement);
|
||||||
} else {
|
that.showSuccess(successDisplayElement)
|
||||||
that.hideSuccess(successDisplayElement);
|
} else {
|
||||||
that.showError(errorDisplayElement, errorMessage);
|
that.hideSuccess(successDisplayElement);
|
||||||
}
|
that.showError(errorDisplayElement, 'Sorry, that name is already taken');
|
||||||
})
|
}
|
||||||
.catch(error => {
|
})
|
||||||
that.hideSuccess(successDisplayElement);
|
.catch(error => {
|
||||||
that.showError(errorDisplayElement, error.message);
|
that.hideSuccess(successDisplayElement);
|
||||||
});
|
that.showError(errorDisplayElement, error.message);
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
that.hideSuccess(successDisplayElement);
|
that.hideSuccess(successDisplayElement);
|
||||||
that.showError(errorDisplayElement, error.message);
|
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
|
// validation function which checks all aspects of a new channel submission
|
||||||
validateNewChannelSubmission: function (userName, password) {
|
validateNewChannelSubmission: function (userName, password) {
|
||||||
const channelName = `@${userName}`;
|
const channelName = `@${userName}`;
|
||||||
|
@ -193,16 +96,15 @@ const validationFunctions = {
|
||||||
return reject(error);
|
return reject(error);
|
||||||
}
|
}
|
||||||
// 3. if all validation passes, check availability of the name
|
// 3. if all validation passes, check availability of the name
|
||||||
that.isNameAvailable(channelName, '/api/channel-is-available/') // validate the availability
|
that.isChannelNameAvailable(channelName)
|
||||||
.then(function(result) {
|
.then(function(isAvailable) {
|
||||||
if (result) {
|
if (isAvailable) {
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
reject(new ChannelNameError('Sorry, that name is already taken'));
|
reject(new ChannelNameError('Sorry, that name is already taken'));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function(error) {
|
.catch(function(error) {
|
||||||
console.log('error evaluating channel name availability', error);
|
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -227,4 +129,4 @@ const validationFunctions = {
|
||||||
resolve();
|
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) {
|
if (result === true) {
|
||||||
res.status(200).json(true);
|
res.status(200).json(true);
|
||||||
} else {
|
} else {
|
||||||
// logger.debug(`Rejecting '${params.name}' because that name has already been claimed by this site`);
|
|
||||||
res.status(200).json(false);
|
res.status(200).json(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -86,7 +85,6 @@ module.exports = (app) => {
|
||||||
if (result === true) {
|
if (result === true) {
|
||||||
res.status(200).json(true);
|
res.status(200).json(true);
|
||||||
} else {
|
} else {
|
||||||
// logger.debug(`Rejecting '${params.name}' because that channel has already been claimed`);
|
|
||||||
res.status(200).json(false);
|
res.status(200).json(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,14 +13,38 @@ module.exports = (app) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// route for log in
|
// route for log in
|
||||||
app.post('/login', passport.authenticate('local-login'), (req, res) => {
|
app.post('/login', (req, res, next) => {
|
||||||
// logger.debug('req.user:', req.user); // req.user contains the authenticated user's info
|
passport.authenticate('local-login', (err, user, info) => {
|
||||||
logger.debug('successful login');
|
logger.debug('info:', info);
|
||||||
res.status(200).json({
|
if (err) {
|
||||||
success : true,
|
return next(err);
|
||||||
channelName : req.user.channelName,
|
}
|
||||||
channelClaimId: req.user.channelClaimId,
|
if (!user) {
|
||||||
shortChannelId: req.user.shortChannelId,
|
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="row row--padded">
|
||||||
<div class="column column--5 column--med-10 align-content-top">
|
<div class="column column--5 column--med-10 align-content-top">
|
||||||
<div class="column column--8 column--med-10">
|
<div class="column column--8 column--med-10">
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
{{> navBar}}
|
||||||
<div class="row row--padded">
|
<div class="row row--padded">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{{#ifConditional this.totalPages '===' 0}}
|
{{#ifConditional this.totalPages '===' 0}}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
{{> navBar}}
|
||||||
<div class="row row--padded">
|
<div class="row row--padded">
|
||||||
<h3>404: Not Found</h3>
|
<h3>404: Not Found</h3>
|
||||||
<p>That page does not exist. Return <a class="link--primary" href="/">home</a>.</p>
|
<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">
|
<div class="row row--tall flex-container--column">
|
||||||
<form>
|
<div id="react-publish-tool" class="row row--padded row--tall flex-container--column">
|
||||||
<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"/>
|
<div class="row row--padded row--tall flex-container--column flex-container--center-center">
|
||||||
</form>
|
<p>loading...</p>
|
||||||
<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)">
|
{{> progressBar}}
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/bundle/bundle.js"></script>
|
||||||
|
|
|
@ -17,8 +17,6 @@
|
||||||
</head>
|
</head>
|
||||||
<body id="channel-body">
|
<body id="channel-body">
|
||||||
<script src="/assets/js/generalFunctions.js"></script>
|
<script src="/assets/js/generalFunctions.js"></script>
|
||||||
<script src="/assets/js/navBarFunctions.js"></script>
|
|
||||||
{{> navBar}}
|
|
||||||
{{{ body }}}
|
{{{ body }}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -18,13 +18,6 @@
|
||||||
<body id="main-body">
|
<body id="main-body">
|
||||||
<script src="/assets/js/generalFunctions.js"></script>
|
<script src="/assets/js/generalFunctions.js"></script>
|
||||||
<script src="/assets/js/validationFunctions.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 }}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -14,9 +14,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body id="show-body">
|
<body id="show-body">
|
||||||
<script src="/assets/js/generalFunctions.js"></script>
|
<script src="/assets/js/generalFunctions.js"></script>
|
||||||
<script src="/assets/js/navBarFunctions.js"></script>
|
|
||||||
<script src="/assets/js/assetConstructor.js"></script>
|
<script src="/assets/js/assetConstructor.js"></script>
|
||||||
{{> navBar}}
|
|
||||||
{{{ body }}}
|
{{{ body }}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
{{> navBar}}
|
||||||
<div class="row row--padded">
|
<div class="row row--padded">
|
||||||
<div class="column column--5 column--med-10 align-content-top">
|
<div class="column column--5 column--med-10 align-content-top">
|
||||||
<div class="column column--8 column--med-10">
|
<div class="column column--8 column--med-10">
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
{{> navBar}}
|
||||||
<div class="row row--padded">
|
<div class="row row--padded">
|
||||||
<h3>No Channel</h3>
|
<h3>No Channel</h3>
|
||||||
<p>There are no published channels matching your url</p>
|
<p>There are no published channels matching your url</p>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
{{> navBar}}
|
||||||
<div class="row row--padded">
|
<div class="row row--padded">
|
||||||
<h3>No Claims</h3>
|
<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>
|
<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>
|
||||||
|
|
||||||
<div id="show-share-buttons">
|
<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">
|
<div class="column column--2 column--med-10">
|
||||||
<span class="text">Share:</span>
|
<span class="text">Share:</span>
|
||||||
</div><div class="column column--7 column--med-10">
|
</div><div class="column column--7 column--med-10">
|
||||||
|
@ -64,14 +64,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row row--wide">
|
<div id="show-details" class="row row--padded row--no-bottom row--wide" hidden="true">
|
||||||
<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-claim-name">
|
<div id="show-claim-name">
|
||||||
<div class="column column--2 column--med-10">
|
<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">
|
</div><div class="column column--8 column--med-10">
|
||||||
{{claimInfo.name}}
|
{{claimInfo.name}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -83,13 +79,6 @@
|
||||||
{{claimInfo.claimId}}
|
{{claimInfo.claimId}}
|
||||||
</div>
|
</div>
|
||||||
</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 id="show-claim-id">
|
||||||
<div class="column column--2 column--med-10">
|
<div class="column column--2 column--med-10">
|
||||||
<span class="text">File Type:</span>
|
<span class="text">File Type:</span>
|
||||||
|
@ -102,3 +91,37 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div id="channel-publish-in-progress" hidden="true">
|
||||||
<p>Creating your new channel. This may take a few seconds...</p>
|
<p>Creating your new channel. This may take a few seconds...</p>
|
||||||
<div id="create-channel-progress-bar"></div>
|
{{> progressBar}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="channel-publish-done" hidden="true">
|
<div id="channel-publish-done" hidden="true">
|
||||||
<p>Your channel has been successfully created!</p>
|
<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">
|
<div class="row row--wide">
|
||||||
<button class="button--primary" onclick="loginToChannel(event)">Authenticate</button>
|
<button class="button--primary" onclick="loginToChannel(event)">Authenticate</button>
|
||||||
</div>
|
</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>
|
<span class="nav-bar-tagline">Open-source, decentralized image and video sharing.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-bar--right">
|
<div class="nav-bar--right">
|
||||||
<a class="nav-bar-link link--nav" href="/">Upload</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="/popular">Popular</a>-->
|
||||||
<a class="nav-bar-link link--nav" href="/about">About</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}}>
|
{{#if user }}
|
||||||
<option id="nav-bar-channel-select-channel-option">@{{user.userName}}</option>
|
<select type="text" id="nav-bar-channel-select" class="select select--arrow link--nav" onchange="toggleNavBarSelection(event)">
|
||||||
<option value="VIEW">View</option>
|
<option id="nav-bar-channel-select-channel-option" >{{user.channelName}}</option>
|
||||||
<option value="LOGOUT">Logout</option>
|
<option value="VIEW" data-channelUrl="/{{user.channelName}}:{{user.channelClaimId}}">View</option>
|
||||||
</select>
|
<option value="LOGOUT">Logout</option>
|
||||||
<a id="nav-bar-login-link" class="nav-bar-link link--nav" href="/login" {{#if user}}style="display:none"{{/if}}>Channel</a>
|
</select>
|
||||||
|
{{else}}
|
||||||
|
<a id="nav-bar-login-link" class="nav-bar-link link--nav" href="/login">Channel</a>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,4 +51,18 @@
|
||||||
link.setAttribute('class', 'select select--arrow link--nav-active');
|
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="row row--padded">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{{#each trendingAssets}}
|
{{#each trendingAssets}}
|
||||||
|
@ -20,4 +21,4 @@
|
||||||
percentPosition: true
|
percentPosition: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
{{> navBar}}
|
||||||
<div class="row row--padded">
|
<div class="row row--padded">
|
||||||
<h3>Error</h3>
|
<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>
|
<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="row row--tall row--padded">
|
||||||
<div class="column column--10">
|
<div class="column column--10">
|
||||||
<!-- title -->
|
<!-- title -->
|
||||||
|
@ -14,4 +15,4 @@
|
||||||
{{> assetInfo}}
|
{{> assetInfo}}
|
||||||
</div>
|
</div>
|
||||||
</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