updates in component state
updates work asset upload works channelForm component
This commit is contained in:
parent
c86b376279
commit
16bcaf7ceb
11 changed files with 467 additions and 40 deletions
34
src/ui/component/channelForm/index.js
Normal file
34
src/ui/component/channelForm/index.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
makeSelectTitleForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
makeSelectCoverForUri,
|
||||
selectCurrentChannelPage,
|
||||
makeSelectMetadataItemForUri,
|
||||
doUpdateChannel,
|
||||
makeSelectAmountForUri,
|
||||
} from 'lbry-redux';
|
||||
import ChannelPage from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
cover: makeSelectCoverForUri(props.uri)(state),
|
||||
page: selectCurrentChannelPage(state),
|
||||
description: makeSelectMetadataItemForUri(props.uri, 'description')(state),
|
||||
website: makeSelectMetadataItemForUri(props.uri, 'website_url')(state),
|
||||
email: makeSelectMetadataItemForUri(props.uri, 'email')(state),
|
||||
tags: makeSelectMetadataItemForUri(props.uri, 'tags')(state),
|
||||
locations: makeSelectMetadataItemForUri(props.uri, 'locations')(state),
|
||||
languages: makeSelectMetadataItemForUri(props.uri, 'languages')(state),
|
||||
amount: makeSelectAmountForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
updateChannel: params => dispatch(doUpdateChannel(params)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(ChannelPage);
|
208
src/ui/component/channelForm/view.jsx
Normal file
208
src/ui/component/channelForm/view.jsx
Normal file
|
@ -0,0 +1,208 @@
|
|||
// @flow
|
||||
import React, { useState } from 'react';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import { Form, FormField } from 'component/common/form';
|
||||
import Button from 'component/button';
|
||||
|
||||
import SelectAsset from '../../component/selectAsset/view';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
||||
title: ?string,
|
||||
amount: string,
|
||||
cover: ?string,
|
||||
thumbnail: ?string,
|
||||
location: { search: string },
|
||||
description: string,
|
||||
website: string,
|
||||
email: string,
|
||||
balance: number,
|
||||
tags: Array<string>,
|
||||
locations: Array<string>,
|
||||
languages: Array<string>,
|
||||
|
||||
updateChannel: any => void,
|
||||
|
||||
updateThumb: string => void,
|
||||
updateCover: string => void,
|
||||
setEditing: boolean => void,
|
||||
};
|
||||
|
||||
function ChannelForm(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
title,
|
||||
cover,
|
||||
description,
|
||||
website,
|
||||
email,
|
||||
thumbnail,
|
||||
tags,
|
||||
locations,
|
||||
languages,
|
||||
amount,
|
||||
updateChannel,
|
||||
setEditing,
|
||||
updateThumb,
|
||||
updateCover,
|
||||
} = props;
|
||||
const { claimId } = parseURI(uri);
|
||||
|
||||
// fill this in with sdk data
|
||||
const channelParams = {
|
||||
website: website,
|
||||
email: email,
|
||||
languages: languages || [],
|
||||
cover: cover,
|
||||
description: description,
|
||||
locations: locations || [],
|
||||
title: title,
|
||||
thumbnail: thumbnail,
|
||||
tags: tags || [],
|
||||
claim_id: claimId,
|
||||
amount: amount,
|
||||
};
|
||||
|
||||
const [params, setParams] = useState(channelParams);
|
||||
const [bidError, setBidError] = useState('');
|
||||
|
||||
const MINIMUM_PUBLISH_BID = 0.00000001;
|
||||
// If a user changes tabs, update the url so it stays on the same page if they refresh.
|
||||
// We don't want to use links here because we can't animate the tab change and using links
|
||||
// would alter the Tab label's role attribute, which should stay role="tab" to work with keyboards/screen readers.
|
||||
|
||||
const handleBidChange = (bid: number) => {
|
||||
const { balance, amount } = props;
|
||||
const totalAvailableBidAmount = parseFloat(amount) + parseFloat(balance);
|
||||
setParams({ ...params, amount: bid });
|
||||
setBidError('');
|
||||
if (bid <= 0.0 || isNaN(bid)) {
|
||||
setBidError(__('Deposit cannot be 0'));
|
||||
} else if (totalAvailableBidAmount === bid) {
|
||||
setBidError(__('Please decrease your deposit to account for transaction fees'));
|
||||
} else if (totalAvailableBidAmount < bid) {
|
||||
setBidError(__('Deposit cannot be higher than your balance'));
|
||||
} else if (bid <= MINIMUM_PUBLISH_BID) {
|
||||
setBidError(__('Your deposit must be higher'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbnailChange = (url: string) => {
|
||||
setParams({ ...params, thumbnail: url });
|
||||
updateThumb(url);
|
||||
};
|
||||
|
||||
const handleCoverChange = (url: string) => {
|
||||
setParams({ ...params, cover: url });
|
||||
updateCover(url);
|
||||
};
|
||||
// TODO clear and bail after submit
|
||||
return (
|
||||
<div className={'card--section'}>
|
||||
<section>
|
||||
<div className={'card__title--flex-between'}>
|
||||
<header className="card__header">
|
||||
<h2 className="card__title">{__('Edit')}</h2>
|
||||
</header>
|
||||
<div className={'card__actions'}>
|
||||
<Button button="primary" label={__('Submit')} onClick={() => updateChannel(params)} />
|
||||
|
||||
<Button
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
onClick={() => {
|
||||
setParams({ ...channelParams });
|
||||
setEditing(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Form onSubmit={channelParams => updateChannel(channelParams)}>
|
||||
<div className="card__content">
|
||||
<SelectAsset
|
||||
onUpdate={v => handleThumbnailChange(v)}
|
||||
currentValue={params.thumbnail}
|
||||
assetName={'Thumbnail'}
|
||||
recommended={'(400x400)'}
|
||||
/>
|
||||
|
||||
<SelectAsset
|
||||
onUpdate={v => handleCoverChange(v)}
|
||||
currentValue={params.cover}
|
||||
assetName={'Cover'}
|
||||
recommended={'(1000x300)'}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
type="text"
|
||||
name="channel_title2"
|
||||
label={__('Title')}
|
||||
placeholder={__('Titular Title')}
|
||||
disabled={false}
|
||||
value={params.title}
|
||||
onChange={e => setParams({ ...params, title: e.target.value })}
|
||||
/>
|
||||
<FormField
|
||||
className="form-field--price-amount"
|
||||
type="number"
|
||||
name="content_bid2"
|
||||
step="any"
|
||||
label={__('Deposit (LBC)')}
|
||||
postfix="LBC"
|
||||
value={params.amount}
|
||||
error={bidError}
|
||||
min="0.0"
|
||||
disabled={false}
|
||||
onChange={event => handleBidChange(parseFloat(event.target.value))}
|
||||
placeholder={0.1}
|
||||
// helper={
|
||||
// <BidHelpText
|
||||
// uri={shortUri}
|
||||
// isResolvingUri={isResolvingUri}
|
||||
// amountNeededForTakeover={amountNeededForTakeover}
|
||||
// />
|
||||
// }
|
||||
/>
|
||||
|
||||
<FormField
|
||||
type="text"
|
||||
name="channel_website2"
|
||||
label={__('Website')}
|
||||
placeholder={__('aprettygoodsite.com')}
|
||||
disabled={false}
|
||||
value={params.website}
|
||||
onChange={e => setParams({ ...params, website: e.target.value })}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
type="text"
|
||||
name="content_email2"
|
||||
label={__('Email')}
|
||||
placeholder={__('yourstruly@example.com')}
|
||||
disabled={false}
|
||||
value={params.email}
|
||||
onChange={e => setParams({ ...params, email: e.target.value })}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
type="markdown"
|
||||
name="content_description2"
|
||||
label={__('Description')}
|
||||
placeholder={__('Description of your content')}
|
||||
value={params.description}
|
||||
disabled={false}
|
||||
onChange={text => setParams({ ...params, description: text })}
|
||||
/>
|
||||
<div className={'card__actions'}>
|
||||
<Button button="primary" label={__('Submit')} onClick={() => updateChannel(params)} />
|
||||
<Button button="link" label={__('Cancel')} onClick={() => setParams({ ...channelParams })} />
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChannelForm;
|
|
@ -8,10 +8,11 @@ type Props = {
|
|||
thumbnail: ?string,
|
||||
uri: string,
|
||||
className?: string,
|
||||
thumbnailPreview: ?string,
|
||||
};
|
||||
|
||||
function ChannelThumbnail(props: Props) {
|
||||
const { thumbnail, uri, className } = props;
|
||||
const { thumbnail, uri, className, thumbnailPreview } = props;
|
||||
|
||||
// Generate a random color class based on the first letter of the channel name
|
||||
const { channelName } = parseURI(uri);
|
||||
|
@ -24,8 +25,8 @@ function ChannelThumbnail(props: Props) {
|
|||
[colorClassName]: !thumbnail,
|
||||
})}
|
||||
>
|
||||
{!thumbnail && <img className="channel-thumbnail__default" src={Gerbil} />}
|
||||
{thumbnail && <img className="channel-thumbnail__custom" src={thumbnail} />}
|
||||
{!thumbnail && <img className="channel-thumbnail__default" src={thumbnailPreview || Gerbil} />}
|
||||
{thumbnail && <img className="channel-thumbnail__custom" src={thumbnailPreview || thumbnail} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ type FileFilters = {
|
|||
|
||||
type Props = {
|
||||
type: string,
|
||||
currentPath: ?string,
|
||||
currentPath?: ?string,
|
||||
onFileChosen: (string, string) => void,
|
||||
label?: string,
|
||||
placeholder?: string,
|
||||
|
|
12
src/ui/component/selectAsset/index.js
Normal file
12
src/ui/component/selectAsset/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import SelectThumbnail from './view';
|
||||
|
||||
const perform = dispatch => ({
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
perform
|
||||
)(SelectThumbnail);
|
BIN
src/ui/component/selectAsset/thumbnail-broken.png
Normal file
BIN
src/ui/component/selectAsset/thumbnail-broken.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
BIN
src/ui/component/selectAsset/thumbnail-missing.png
Normal file
BIN
src/ui/component/selectAsset/thumbnail-missing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
142
src/ui/component/selectAsset/view.jsx
Normal file
142
src/ui/component/selectAsset/view.jsx
Normal file
|
@ -0,0 +1,142 @@
|
|||
// @flow
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { FormField } from 'component/common/form';
|
||||
import FileSelector from 'component/common/file-selector';
|
||||
import Button from 'component/button';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const filters = [
|
||||
{
|
||||
name: __('Thumbnail Image'),
|
||||
extensions: ['png', 'jpg', 'jpeg', 'gif'],
|
||||
},
|
||||
];
|
||||
|
||||
const SOURCE_URL = 'url';
|
||||
const SOURCE_UPLOAD = 'upload';
|
||||
const SPEECH_READY = 'READY';
|
||||
const SPEECH_UPLOADING = 'UPLOADING';
|
||||
type Props = {
|
||||
assetName: string,
|
||||
currentValue: ?string,
|
||||
onUpdate: string => void,
|
||||
recommended: string,
|
||||
};
|
||||
|
||||
function SelectAsset(props: Props) {
|
||||
const { onUpdate, assetName, currentValue, recommended } = props;
|
||||
const [assetSource, setAssetSource] = useState(SOURCE_URL);
|
||||
const [pathSelected, setPathSelected] = useState('');
|
||||
const [uploadStatus, setUploadStatus] = useState(SPEECH_READY);
|
||||
|
||||
function doUploadAsset(filePath, thumbnailBuffer) {
|
||||
let thumbnail, fileExt, fileName, fileType;
|
||||
if (filePath) {
|
||||
thumbnail = fs.readFileSync(filePath);
|
||||
fileExt = path.extname(filePath);
|
||||
fileName = path.basename(filePath);
|
||||
fileType = `image/${fileExt.slice(1)}`;
|
||||
} else if (thumbnailBuffer) {
|
||||
thumbnail = thumbnailBuffer;
|
||||
fileExt = '.png';
|
||||
fileName = 'thumbnail.png';
|
||||
fileType = 'image/png';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uploadError = (error = '') => {
|
||||
console.log('error', error);
|
||||
};
|
||||
|
||||
const setUrl = path => {
|
||||
setUploadStatus(SPEECH_READY);
|
||||
onUpdate(path);
|
||||
setAssetSource(SOURCE_URL);
|
||||
};
|
||||
|
||||
setUploadStatus(SPEECH_UPLOADING);
|
||||
|
||||
const data = new FormData();
|
||||
const name = uuidv4();
|
||||
const file = new File([thumbnail], fileName, { type: fileType });
|
||||
data.append('name', name);
|
||||
data.append('file', file);
|
||||
|
||||
return fetch('https://spee.ch/api/claim/publish', {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(json => (json.success ? setUrl(`${json.data.serveUrl}`) : uploadError(json.message)))
|
||||
.catch(err => uploadError(err.message));
|
||||
}
|
||||
return (
|
||||
<fieldset-section>
|
||||
<fieldset-group className="fieldset-group--smushed">
|
||||
<FormField
|
||||
type="select"
|
||||
name={assetName}
|
||||
value={assetSource}
|
||||
onChange={e => setAssetSource(e.target.value)}
|
||||
label={__(assetName + ' source')}
|
||||
>
|
||||
<option key={'lmmnop'} value={'url'}>
|
||||
URL
|
||||
</option>
|
||||
<option key={'lmmnopq'} value={'upload'}>
|
||||
UPLOAD
|
||||
</option>
|
||||
</FormField>
|
||||
{assetSource === SOURCE_UPLOAD && (
|
||||
<>
|
||||
{!pathSelected && (
|
||||
<FileSelector
|
||||
label={'File to upload'}
|
||||
name={'assetSelector'}
|
||||
onFileChosen={path => {
|
||||
setPathSelected(path);
|
||||
}}
|
||||
filters={filters}
|
||||
/>
|
||||
)}
|
||||
{pathSelected && (
|
||||
<div>
|
||||
{`...${pathSelected.slice(-18)}`} {uploadStatus}{' '}
|
||||
<Button button={'primary'} onClick={() => doUploadAsset(pathSelected)}>
|
||||
Upload
|
||||
</Button>{' '}
|
||||
<Button
|
||||
button={'secondary'}
|
||||
onClick={() => {
|
||||
setPathSelected('');
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{assetSource === SOURCE_URL && (
|
||||
<FormField
|
||||
type={'text'}
|
||||
name={'thumbnail'}
|
||||
label={__(assetName + ' ' + recommended)}
|
||||
placeholder={__('https://example.com/image.png')}
|
||||
disabled={false}
|
||||
value={currentValue}
|
||||
onChange={e => {
|
||||
onUpdate(e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</fieldset-group>
|
||||
</fieldset-section>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectAsset;
|
|
@ -1,15 +1,18 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import Page from 'component/page';
|
||||
import SubscribeButton from 'component/subscribeButton';
|
||||
import ShareButton from 'component/shareButton';
|
||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||
import { withRouter } from 'react-router';
|
||||
import Button from 'component/button';
|
||||
import { formatLbryUriForWeb } from 'util/uri';
|
||||
import ChannelContent from 'component/channelContent';
|
||||
import ChannelAbout from 'component/channelAbout';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import ChannelForm from 'component/channelForm';
|
||||
import * as ICONS from 'constants/icons';
|
||||
|
||||
const PAGE_VIEW_QUERY = `view`;
|
||||
const ABOUT_PAGE = `about`;
|
||||
|
@ -23,15 +26,20 @@ type Props = {
|
|||
location: { search: string },
|
||||
history: { push: string => void },
|
||||
match: { params: { attribute: ?string } },
|
||||
channelIsMine: boolean,
|
||||
};
|
||||
|
||||
function ChannelPage(props: Props) {
|
||||
const { uri, title, cover, history, location, page } = props;
|
||||
const { uri, title, cover, history, location, page, channelIsMine, thumbnail } = props;
|
||||
const { channelName, claimName, claimId } = parseURI(uri);
|
||||
const { search } = location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined;
|
||||
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [thumbPreview, setThumbPreview] = useState(thumbnail);
|
||||
const [coverPreview, setCoverPreview] = useState(cover);
|
||||
|
||||
// If a user changes tabs, update the url so it stays on the same page if they refresh.
|
||||
// We don't want to use links here because we can't animate the tab change and using links
|
||||
// would alter the Tab label's role attribute, which should stay role="tab" to work with keyboards/screen readers.
|
||||
|
@ -52,40 +60,59 @@ function ChannelPage(props: Props) {
|
|||
<Page>
|
||||
<div className="card">
|
||||
<header className="channel-cover">
|
||||
{cover && <img className="channel-cover__custom" src={cover} />}
|
||||
|
||||
<div className="channel__primary-info">
|
||||
<ChannelThumbnail className="channel__thumbnail--channel-page" uri={uri} />
|
||||
|
||||
<div>
|
||||
<h1 className="channel__title">{title || channelName}</h1>
|
||||
<h2 className="channel__url">
|
||||
{claimName}
|
||||
{claimId && `#${claimId}`}
|
||||
</h2>
|
||||
</div>
|
||||
{!editing && cover && <img className="channel-cover__custom" src={cover} />}
|
||||
{editing && <img className="channel-cover__custom" src={coverPreview} />}
|
||||
{/* component that offers select/upload */}
|
||||
<div className="channel__primary-info ">
|
||||
{!editing && <ChannelThumbnail className="channel__thumbnail--channel-page" uri={uri} />}
|
||||
{editing && (
|
||||
<ChannelThumbnail
|
||||
className="channel__thumbnail--channel-page"
|
||||
uri={uri}
|
||||
thumbnailPreview={thumbPreview}
|
||||
/>
|
||||
)}
|
||||
<h1 className="channel__title">
|
||||
{title || channelName}
|
||||
{channelIsMine && !editing && (
|
||||
<Button onClick={() => setEditing(!editing)} icon={ICONS.EDIT} iconSize={49} />
|
||||
)}
|
||||
</h1>
|
||||
<h2 className="channel__url">
|
||||
{claimName}
|
||||
{claimId && `#${claimId}`}
|
||||
</h2>
|
||||
</div>
|
||||
</header>
|
||||
{!editing && (
|
||||
<Tabs onChange={onTabChange} index={tabIndex}>
|
||||
<TabList className="tabs__list--channel-page">
|
||||
<Tab>{__('Content')}</Tab>
|
||||
<Tab>{__('About')}</Tab>
|
||||
<div className="card__actions">
|
||||
<ShareButton uri={uri} />
|
||||
<SubscribeButton uri={uri} />
|
||||
</div>
|
||||
</TabList>
|
||||
|
||||
<Tabs onChange={onTabChange} index={tabIndex}>
|
||||
<TabList className="tabs__list--channel-page">
|
||||
<Tab>{__('Content')}</Tab>
|
||||
<Tab>{__('About')}</Tab>
|
||||
<div className="card__actions">
|
||||
<ShareButton uri={uri} />
|
||||
<SubscribeButton uri={uri} />
|
||||
</div>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<ChannelContent uri={uri} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ChannelAbout uri={uri} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<ChannelContent uri={uri} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ChannelAbout uri={uri} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
)}
|
||||
{editing && (
|
||||
<ChannelForm
|
||||
uri={uri}
|
||||
setEditing={setEditing}
|
||||
updateThumb={v => setThumbPreview(v)}
|
||||
updateCover={v => setCoverPreview(v)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
|
|
|
@ -345,7 +345,7 @@ export const doPublish = () => (dispatch: Dispatch, getState: () => {}) => {
|
|||
dispatch({ type: ACTIONS.PUBLISH_FAIL });
|
||||
dispatch(doError(error.message));
|
||||
};
|
||||
|
||||
console.log('PP', publishPayload);
|
||||
return Lbry.publish(publishPayload).then(success, failure);
|
||||
};
|
||||
|
||||
|
|
|
@ -286,5 +286,8 @@
|
|||
"Multi-language support is brand new and incomplete. Switching your language may have unintended consequences.": "Multi-language support is brand new and incomplete. Switching your language may have unintended consequences.",
|
||||
"Wallet": "Wallet",
|
||||
"Home": "Home",
|
||||
"Following": "Following"
|
||||
}
|
||||
"Following": "Following",
|
||||
"Update ready to install": "Update ready to install",
|
||||
"Install now": "Install now",
|
||||
"Edit": "Edit"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue