added flux to anonymous/channel select

This commit is contained in:
bill bittner 2018-01-09 18:25:38 -08:00
parent 66e1496a73
commit 67bf5b74f9
12 changed files with 242 additions and 175 deletions

View file

@ -1,7 +1,10 @@
// export action types // export action types
export const FILE_SELECTED = 'FILE_SELECTED'; export const FILE_SELECTED = 'FILE_SELECTED';
export const FILE_CLEAR = 'FILE_CLEAR'; export const FILE_CLEAR = 'FILE_CLEAR';
export const METADATA_UPDATE = 'TITLE_UPDATE'; export const METADATA_UPDATE = 'METADATA_UPDATE';
export const CLAIM_UPDATE = 'CLAIM_UPDATE';
export const CHANNEL_UPDATE = 'CHANNEL_UPDATE';
export const SET_PUBLISH_IN_CHANNEL = 'SET_PUBLISH_IN_CHANNEL';
// export action creators // export action creators
export function selectFile (file) { export function selectFile (file) {
@ -24,3 +27,26 @@ export function updateMetadata (name, value) {
value, value,
}; };
}; };
export function updateClaim (value) {
return {
type: CLAIM_UPDATE,
value,
};
};
export function updateLoggedInChannel (name, shortId, longId) {
return {
type: CHANNEL_UPDATE,
name,
shortId,
longId,
};
};
export function setPublishInChannel (value) {
return {
type: SET_PUBLISH_IN_CHANNEL,
value,
};
}

View file

@ -1,4 +1,6 @@
import React from 'react'; import React from 'react';
import { setPublishInChannel } from '../actions';
import { connect } from 'react-redux';
class AnonymousOrChannelSelect extends React.Component { class AnonymousOrChannelSelect extends React.Component {
constructor (props) { constructor (props) {
@ -8,9 +10,9 @@ class AnonymousOrChannelSelect extends React.Component {
toggleAnonymousPublish (event) { toggleAnonymousPublish (event) {
const value = event.target.value; const value = event.target.value;
if (value === 'anonymous') { if (value === 'anonymous') {
this.props.updateUploaderState('publishToChannel', false); this.props.onPublishToChannelChange(false);
} else { } else {
this.props.updateUploaderState('publishToChannel', true); this.props.onPublishToChannelChange(true);
} }
} }
render () { render () {
@ -19,11 +21,11 @@ class AnonymousOrChannelSelect extends React.Component {
<div className="column column--10"> <div className="column column--10">
<form> <form>
<div className="column column--3 column--med-10"> <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.publishToChannel} onChange={this.toggleAnonymousPublish}/> <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> <label className="label label--pointer" htmlFor="anonymous-radio">Anonymous</label>
</div> </div>
<div className="column column--7 column--med-10"> <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.publishToChannel} onChange={this.toggleAnonymousPublish}/> <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> <label className="label label--pointer" htmlFor="channel-radio">In a channel</label>
</div> </div>
</form> </form>
@ -33,4 +35,18 @@ class AnonymousOrChannelSelect extends React.Component {
} }
} }
module.exports = AnonymousOrChannelSelect; const mapStateToProps = state => {
return {
publishInChannel: state.publishInChannel,
};
};
const mapDispatchToProps = dispatch => {
return {
onPublishToChannelChange: (value) => {
dispatch(setPublishInChannel(value));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AnonymousOrChannelSelect);

View file

@ -28,6 +28,11 @@ class ChannelSelector extends React.Component {
selectOption (option) { selectOption (option) {
this.setState({optionState: option}); this.setState({optionState: option});
} }
updateLoggedInChannelOutsideReact (channelName, channelClaimId, shortChannelId) {
// update anywhere on page that needs to be updated outside of this component
setUserCookies(channelName, channelClaimId, shortChannelId);
this.replaceChannelSelectionInNavBar(channelName);
}
replaceChannelSelectionInNavBar (loggedInChannel) { replaceChannelSelectionInNavBar (loggedInChannel) {
// remove the old channel option // remove the old channel option
const oldChannel = document.getElementById('nav-bar-channel-select-channel-option'); const oldChannel = document.getElementById('nav-bar-channel-select-channel-option');
@ -48,11 +53,6 @@ class ChannelSelector extends React.Component {
const navBarLoginLink = document.getElementById('nav-bar-login-link'); const navBarLoginLink = document.getElementById('nav-bar-login-link');
navBarLoginLink.style.display = 'none'; navBarLoginLink.style.display = 'none';
} }
updateLoggedInChannelOutsideReact (channelName, channelClaimId, shortChannelId) {
// update anywhere on page that needs to be updated outside of this component
setUserCookies(channelName, channelClaimId, shortChannelId);
this.replaceChannelSelectionInNavBar(channelName);
}
render () { render () {
return ( return (
<div> <div>

View file

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import {connect} from 'react-redux';
class PreviewDropzone extends React.Component { class PreviewDropzone extends React.Component {
constructor (props) { constructor (props) {
@ -39,4 +40,10 @@ class PreviewDropzone extends React.Component {
} }
}; };
module.exports = PreviewDropzone; const mapStateToProps = state => {
return {
file: state.file,
};
};
export default connect(mapStateToProps, null)(PreviewDropzone);

View file

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
// import PropTypes from 'prop-types'; // import PropTypes from 'prop-types';
import { selectFile } from '../actions'; import { selectFile } from '../actions';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -18,7 +17,6 @@ class PublishDropzone extends React.Component {
this.handleDragLeave = this.handleDragLeave.bind(this); this.handleDragLeave = this.handleDragLeave.bind(this);
this.handleClick = this.handleClick.bind(this); this.handleClick = this.handleClick.bind(this);
this.handleFileInput = this.handleFileInput.bind(this); this.handleFileInput = this.handleFileInput.bind(this);
this.setClaimNameFromFileName = this.setClaimNameFromFileName.bind(this);
} }
validateFile (file) { validateFile (file) {
if (!file) { if (!file) {
@ -113,16 +111,9 @@ class PublishDropzone extends React.Component {
return this.setState({fileError: error.message}); return this.setState({fileError: error.message});
} }
// stage it so it will be ready when the publish button is clicked // stage it so it will be ready when the publish button is clicked
this.setClaimNameFromFileName(chosenFile.name);
this.props.onFileSelect(chosenFile); this.props.onFileSelect(chosenFile);
} }
} }
setClaimNameFromFileName (fileName) {
console.log('setClaimNameFromFileName', fileName);
const fileNameWithoutEnding = fileName.substring(0, fileName.lastIndexOf('.'));
const cleanClaimName = this.props.cleanseInput(fileNameWithoutEnding);
this.props.updateUploaderState('claim', cleanClaimName);
}
render () { render () {
return ( return (
<div className="row row--tall flex-container--column"> <div className="row row--tall flex-container--column">

View file

@ -7,8 +7,9 @@ import PublishThumbnailInput from './PublishThumbnailInput.jsx';
import PublishMetadataInputs from './PublishMetadataInputs.jsx'; import PublishMetadataInputs from './PublishMetadataInputs.jsx';
import AnonymousOrChannelSelect from './AnonymousOrChannelSelect.jsx'; import AnonymousOrChannelSelect from './AnonymousOrChannelSelect.jsx';
import { selectFile, clearFile } from '../actions'; import {selectFile, clearFile, updateLoggedInChannel} from '../actions';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getCookie } from '../utils/cookies.js';
class PublishForm extends React.Component { class PublishForm extends React.Component {
constructor (props) { constructor (props) {
@ -20,6 +21,14 @@ class PublishForm extends React.Component {
}; };
this.publish = this.publish.bind(this); this.publish = this.publish.bind(this);
} }
componentWillMount () {
// check for whether a channel is logged in
// if so, set the loggedInChannel to the channel name
const loggedInChannelName = getCookie('channel_name');
const loggedInChannelShortId = getCookie('short_channel_id');
const loggedInChannelLongId = getCookie('long_channel_id');
this.props.onChannelUpdate(loggedInChannelName, loggedInChannelShortId, loggedInChannelLongId);
}
publish () { publish () {
// publish the asset // publish the asset
} }
@ -34,33 +43,16 @@ class PublishForm extends React.Component {
<div className="column column--5 column--sml-10" > <div className="column column--5 column--sml-10" >
<div className="row row--padded"> <div className="row row--padded">
<PreviewDropzone <PreviewDropzone />
file={this.props.file} { (this.props.fileType === 'video/mp4') && <PublishThumbnailInput /> }
/>
{ (this.props.file.type === 'video/mp4') &&
<PublishThumbnailInput
thumbnail={this.props.thumbnail}
/>
}
</div> </div>
</div> </div>
<div className="column column--5 column--sml-10 align-content-top"> <div className="column column--5 column--sml-10 align-content-top">
<div id="publish-active-area" className="row row--padded"> <div id="publish-active-area" className="row row--padded">
<PublishUrlInput <PublishUrlInput />
fileName={this.props.file.name} <AnonymousOrChannelSelect />
claim={this.props.claim}
publishToChannel={this.props.publishToChannel}
loggedInChannelName={this.props.loggedInChannelName}
loggedInChannelShortId={this.props.loggedInChannelShortId}
cleanseInput={this.props.cleanseInput}
updateUploaderState={this.props.updateUploaderState}
makeGetRequest={this.props.makeGetRequest}
/>
<AnonymousOrChannelSelect
publishToChannel={this.props.publishToChannel}
updateUploaderState={this.props.updateUploaderState}
/>
<ChannelSelector <ChannelSelector
loggedInChannelName={this.props.loggedInChannelName} loggedInChannelName={this.props.loggedInChannelName}
publishToChannel={this.props.publishToChannel} publishToChannel={this.props.publishToChannel}
@ -96,13 +88,10 @@ class PublishForm extends React.Component {
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
loggedInChannelName : state.loggedInChannelName, fileType : state.file.type,
loggedInChannelShortId: state.loggedInChannelShortId,
publishToChannel : state.publishToChannel,
file : state.file,
claim : state.claim, claim : state.claim,
thumbnail : state.thumbnail, thumbnail : state.thumbnail,
description : state.description, description: state.description,
license : state.license, license : state.license,
nsfw : state.nsfw, nsfw : state.nsfw,
}; };
@ -116,7 +105,10 @@ const mapDispatchToProps = dispatch => {
onFileClear: () => { onFileClear: () => {
dispatch(clearFile()); dispatch(clearFile());
}, },
onChannelUpdate: (name, shortId, longId) => {
dispatch(updateLoggedInChannel(name, shortId, longId));
},
}; };
} };
export default connect(mapStateToProps, mapDispatchToProps)(PublishForm); export default connect(mapStateToProps, mapDispatchToProps)(PublishForm);

View file

@ -7,104 +7,13 @@ import {connect} from 'react-redux';
class PublishTool extends React.Component { class PublishTool extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
// bind class methods with `this`
this.updateUploaderState = this.updateUploaderState.bind(this);
this.makeGetRequest = this.makeGetRequest.bind(this);
this.makePostRequest = this.makePostRequest.bind(this);
this.cleanseInput = this.cleanseInput.bind(this);
this.getCookie = this.getCookie.bind(this);
}
componentDidMount () {
// check for whether a channel is logged in
// if so, setState loggedInChannel to the channel name
const loggedInChannelName = this.getCookie('channel_name');
this.setState({loggedInChannelName})
const loggedInChannelShortId = this.getCookie('short_channel_id');
this.setState({loggedInChannelShortId});
}
updateUploaderState (name, value) {
console.log(`updateUploaderState ${name} ${value}`);
this.setState({[name]: value});
}
makeGetRequest (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();
});
}
makePostRequest (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('Wrong channel name or password');
} else {
reject('request failed with status:' + xhttp.status);
};
}
};
xhttp.send(params);
});
}
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;
}
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 '';
} }
render () { render () {
return ( return (
<div className="row row--tall flex-container--column"> <div className="row row--tall flex-container--column">
{ !this.props.file && { !this.props.file && <PublishDropzone /> }
<PublishDropzone { this.props.file && <PublishForm /> }
updateUploaderState={this.updateUploaderState} { this.props.publishStatus && <PublishStatus /> }
cleanseInput={this.cleanseInput}
/>
}
{ this.props.file &&
<PublishForm
updateUploaderState={this.updateUploaderState}
makeGetRequest={this.makeGetRequest}
makePostRequest={this.makePostRequest}
cleanseInput={this.cleanseInput}
/>
}
{ this.props.publishStatus &&
<PublishStatus />
}
</div> </div>
); );
} }
@ -112,7 +21,8 @@ class PublishTool extends React.Component {
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
file: state.file, file : state.file,
publishStatus: state.publishStatus,
}; };
}; };

View file

@ -1,17 +1,8 @@
import React from 'react'; import React from 'react';
import { updateClaim } from '../actions';
function UrlMiddle ({publishToChannel, loggedInChannelName, loggedInChannelShortId}) { import { connect } from 'react-redux';
if (publishToChannel) { import { makeGetRequest } from '../utils/xhr.js';
if (loggedInChannelName) { import UrlMiddle from './UrlMiddle.jsx';
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>
);
}
class UrlChooser extends React.Component { class UrlChooser extends React.Component {
constructor (props) { constructor (props) {
@ -22,19 +13,39 @@ class UrlChooser extends React.Component {
urlMiddle: null, urlMiddle: null,
}; };
this.handleInput = this.handleInput.bind(this); this.handleInput = this.handleInput.bind(this);
this.cleanseInput = this.cleanseInput.bind(this);
this.setClaimNameFromFileName = this.setClaimNameFromFileName.bind(this);
this.checkClaimIsAvailable = this.checkClaimIsAvailable.bind(this); this.checkClaimIsAvailable = this.checkClaimIsAvailable.bind(this);
} }
componentWillMount () {
if (!this.props.claim || this.props.claim === '') {
this.setClaimNameFromFileName();
}
}
handleInput (event) { handleInput (event) {
event.preventDefault(); event.preventDefault();
let value = event.target.value; let value = event.target.value;
const name = event.target.name; value = this.cleanseInput(value);
value = this.props.cleanseInput(value); // update the state
this.props.updateUploaderState(name, value); this.props.onClaimChange(value);
// check to make sure claim name is available
this.checkClaimIsAvailable(value); this.checkClaimIsAvailable(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;
console.log('setClaimNameFromFileName', fileName);
const fileNameWithoutEnding = fileName.substring(0, fileName.lastIndexOf('.'));
const cleanClaimName = this.cleanseInput(fileNameWithoutEnding);
this.props.onClaimChange(cleanClaimName);
}
checkClaimIsAvailable (claim) { checkClaimIsAvailable (claim) {
const that = this; const that = this;
this.props.makeGetRequest(`/api/claim-is-available/${claim}`) makeGetRequest(`/api/claim-is-available/${claim}`)
.then(() => { .then(() => {
that.setState({'error': null}); that.setState({'error': null});
}) })
@ -58,9 +69,7 @@ class UrlChooser extends React.Component {
<UrlMiddle publishToChannel={this.props.publishToChannel} loggedInChannelName={this.props.loggedInChannelName} loggedInChannelShortId={this.props.loggedInChannelShortId}/> <UrlMiddle publishToChannel={this.props.publishToChannel} 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}/> <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.state.error) && ( { (this.props.claim && !this.state.error) && <span id="input-success-claim-name" className="info-message--success span--absolute">{'\u2713'}</span> }
<span id="input-success-claim-name" className="info-message--success span--absolute">{'\u2713'}</span>
)}
</div> </div>
@ -70,4 +79,22 @@ class UrlChooser extends React.Component {
} }
} }
module.exports = UrlChooser; const mapStateToProps = state => {
return {
fileName : state.file.name,
loggedInChannelName : state.loggedInChannel.name,
loggedInChannelShortId: state.loggedInChannel.shortId,
publishToChannel : state.publishToChannel,
claim : state.claim,
};
};
const mapDispatchToProps = dispatch => {
return {
onClaimChange: (value) => {
dispatch(updateClaim(value));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(UrlChooser);

View file

@ -0,0 +1,16 @@
import React from 'react';
function UrlMiddle ({publishToChannel, loggedInChannelName, loggedInChannelShortId}) {
if (publishToChannel) {
if (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>
);
}
export default UrlMiddle;

View file

@ -1,15 +1,22 @@
import {FILE_CLEAR, FILE_SELECTED, METADATA_UPDATE} from '../actions'; import {
CHANNEL_UPDATE, CLAIM_UPDATE, FILE_CLEAR, FILE_SELECTED, METADATA_UPDATE,
SET_PUBLISH_IN_CHANNEL,
} from '../actions';
const initialState = { const initialState = {
loggedInChannelName : null, loggedInChannel: {
name : null,
shortId: null,
longId : null,
},
loggedInChannelShortId: null, loggedInChannelShortId: null,
publishToChannel : false, publishInChannel : false,
publishStatus : null, publishStatus : null,
error : null, error : null,
file : null, file : null,
claim : '',
metadata : { metadata : {
title : '', title : '',
claim : '',
thumbnail : '', thumbnail : '',
description: '', description: '',
license : '', license : '',
@ -35,7 +42,23 @@ export default function (state = initialState, action) {
metadata: { metadata: {
[action.name]: action.value, [action.name]: action.value,
}, },
}) });
case CLAIM_UPDATE:
return Object.assign({}, state, {
claim: action.value,
});
case CHANNEL_UPDATE:
return Object.assign({}, state, {
loggedInChannel: {
name : action.name,
shortId: action.shortId,
longId : action.longId,
},
});
case SET_PUBLISH_IN_CHANNEL:
return Object.assign({}, state, {
publishInChannel: action.value,
});
default: default:
return state; return state;
} }

18
react/utils/cookies.js Normal file
View file

@ -0,0 +1,18 @@
module.exports = {
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 '';
},
}

41
react/utils/xhr.js Normal file
View file

@ -0,0 +1,41 @@
module.exports = {
makeGetRequest (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();
});
},
makePostRequest (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('Wrong channel name or password');
} else {
reject('request failed with status:' + xhttp.status);
};
}
};
xhttp.send(params);
});
},
}