style pass for channel edit
clean up sections and improve navigation handling cleanup + image upload modal styling
This commit is contained in:
parent
36f93343f6
commit
259f51abd6
22 changed files with 577 additions and 494 deletions
|
@ -70,7 +70,7 @@
|
|||
"@datapunt/matomo-tracker-js": "^0.1.4",
|
||||
"@exponent/electron-cookies": "^2.0.0",
|
||||
"@hot-loader/react-dom": "^16.8",
|
||||
"@lbry/components": "^4.2.2",
|
||||
"@lbry/components": "^4.2.5",
|
||||
"@reach/menu-button": "0.7.4",
|
||||
"@reach/rect": "^0.2.1",
|
||||
"@reach/tabs": "^0.1.5",
|
||||
|
|
|
@ -77,6 +77,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
'button--primary': button === 'primary',
|
||||
'button--secondary': button === 'secondary',
|
||||
'button--alt': button === 'alt',
|
||||
'button--danger': button === 'danger',
|
||||
'button--inverse': button === 'inverse',
|
||||
'button--close': button === 'close',
|
||||
'button--disabled': disabled,
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
// @flow
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import { FormField } from 'component/common/form';
|
||||
import Button from 'component/button';
|
||||
import TagsSearch from 'component/tagsSearch';
|
||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||
import ErrorText from 'component/common/error-text';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import { isNameValid, parseURI } from 'lbry-redux';
|
||||
import ClaimAbandonButton from 'component/claimAbandonButton';
|
||||
import { MINIMUM_PUBLISH_BID, INVALID_NAME_ERROR, ESTIMATED_FEE } from 'constants/claim';
|
||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||
import Card from 'component/common/card';
|
||||
|
||||
type Props = {
|
||||
claim: ChannelClaim,
|
||||
|
@ -33,7 +35,10 @@ type Props = {
|
|||
createError: string,
|
||||
creatingChannel: boolean,
|
||||
onDone: () => void,
|
||||
openModal: (id: string, { onUpdate: string => void, label: string, helptext: string, currentValue: string }) => void,
|
||||
openModal: (
|
||||
id: string,
|
||||
{ onUpdate: string => void, assetName: string, helpText: string, currentValue: string, title: string }
|
||||
) => void,
|
||||
uri: string,
|
||||
};
|
||||
|
||||
|
@ -59,9 +64,29 @@ function ChannelForm(props: Props) {
|
|||
createError,
|
||||
openModal,
|
||||
} = props;
|
||||
const [params, setParams]: [any, (any) => void] = React.useState(getChannelParams());
|
||||
const [nameError, setNameError] = React.useState(undefined);
|
||||
const [bidError, setBidError] = React.useState('');
|
||||
const { claim_id: claimId } = claim || {};
|
||||
const { channelName } = parseURI(uri);
|
||||
const name = params.name;
|
||||
const isNewChannel = !uri;
|
||||
|
||||
function getChannelParams() {
|
||||
// fill this in with sdk data
|
||||
const channelParams = {
|
||||
const channelParams: {
|
||||
website: string,
|
||||
email: string,
|
||||
cover: string,
|
||||
thumbnail: string,
|
||||
description: string,
|
||||
title: string,
|
||||
amount: number,
|
||||
languages: ?Array<string>,
|
||||
locations: ?Array<string>,
|
||||
tags: ?Array<{ name: string }>,
|
||||
claim_id?: string,
|
||||
} = {
|
||||
website,
|
||||
email,
|
||||
cover,
|
||||
|
@ -82,28 +107,10 @@ function ChannelForm(props: Props) {
|
|||
channelParams['claim_id'] = claimId;
|
||||
}
|
||||
|
||||
const { channelName } = parseURI(uri);
|
||||
const [params, setParams]: [any, (any) => void] = useState(channelParams);
|
||||
const [nameError, setNameError] = useState(undefined);
|
||||
const [bidError, setBidError] = useState('');
|
||||
|
||||
const name = params.name;
|
||||
|
||||
useEffect(() => {
|
||||
let nameError;
|
||||
if (!name && name !== undefined) {
|
||||
nameError = __('A name is required for your url');
|
||||
} else if (!isNameValid(name, false)) {
|
||||
nameError = INVALID_NAME_ERROR;
|
||||
return channelParams;
|
||||
}
|
||||
|
||||
setNameError(nameError);
|
||||
}, [name]);
|
||||
// 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) => {
|
||||
function handleBidChange(bid: number) {
|
||||
const { balance, amount } = props;
|
||||
const totalAvailableBidAmount = parseFloat(amount) || 0.0 + parseFloat(balance) || 0.0;
|
||||
setParams({ ...params, amount: bid });
|
||||
|
@ -119,17 +126,17 @@ function ChannelForm(props: Props) {
|
|||
} else {
|
||||
setBidError('');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleThumbnailChange = (thumbnailUrl: string) => {
|
||||
function handleThumbnailChange(thumbnailUrl: string) {
|
||||
setParams({ ...params, thumbnail: thumbnailUrl });
|
||||
};
|
||||
}
|
||||
|
||||
const handleCoverChange = (coverUrl: string) => {
|
||||
function handleCoverChange(coverUrl: string) {
|
||||
setParams({ ...params, cover: coverUrl });
|
||||
};
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
function handleSubmit() {
|
||||
if (uri) {
|
||||
updateChannel(params).then(success => {
|
||||
if (success) {
|
||||
|
@ -143,28 +150,34 @@ function ChannelForm(props: Props) {
|
|||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
let nameError;
|
||||
if (!name && name !== undefined) {
|
||||
nameError = __('A name is required for your url');
|
||||
} else if (!isNameValid(name, false)) {
|
||||
nameError = INVALID_NAME_ERROR;
|
||||
}
|
||||
|
||||
setNameError(nameError);
|
||||
}, [name]);
|
||||
|
||||
// TODO clear and bail after submit
|
||||
return (
|
||||
<>
|
||||
<div className="main--contained">
|
||||
<header className="channel-cover--edit">
|
||||
<span className={'channel__uri-preview'}>{uri || `lbry://@${params.name || '...'}`}</span>
|
||||
{uri && (
|
||||
<header className="channel-cover">
|
||||
<div className="channel__quick-actions">
|
||||
<ClaimAbandonButton uri={uri} />
|
||||
</div>
|
||||
)}
|
||||
<div className="channel__edit-cover">
|
||||
<Button
|
||||
button="alt"
|
||||
title={__('Cover')}
|
||||
onClick={() =>
|
||||
openModal(MODALS.IMAGE_UPLOAD, {
|
||||
onUpdate: v => handleCoverChange(v),
|
||||
label: 'Cover',
|
||||
helptext: 'This shoul de such a size',
|
||||
onUpdate: coverUrl => handleCoverChange(coverUrl),
|
||||
title: __('Edit Cover Image'),
|
||||
helpText: __('(Y x Z)'),
|
||||
assetName: __('Cover Image'),
|
||||
currentValue: params.cover,
|
||||
})
|
||||
}
|
||||
|
@ -181,8 +194,9 @@ function ChannelForm(props: Props) {
|
|||
onClick={() =>
|
||||
openModal(MODALS.IMAGE_UPLOAD, {
|
||||
onUpdate: v => handleThumbnailChange(v),
|
||||
label: 'Thumbnail',
|
||||
helptext: 'This shoul de such a size',
|
||||
title: __('Edit Thumbnail Image'),
|
||||
helpText: __('(Y x Z)'),
|
||||
assetName: __('Thumbnail'),
|
||||
currentValue: params.thumbnail,
|
||||
})
|
||||
}
|
||||
|
@ -202,21 +216,62 @@ function ChannelForm(props: Props) {
|
|||
</div>
|
||||
<div className="channel-cover__gradient" />
|
||||
</header>
|
||||
<div className="card">
|
||||
<section className={'section card--section'}>
|
||||
{!uri && (
|
||||
|
||||
<Tabs>
|
||||
<TabList className="tabs__list--channel-page">
|
||||
<Tab>{__('General')}</Tab>
|
||||
<Tab>{__('LBC Details')}</Tab>
|
||||
<Tab>{__('Tags')}</Tab>
|
||||
<Tab>{__('Other')}</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Card
|
||||
body={
|
||||
<>
|
||||
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
|
||||
<fieldset-section>
|
||||
<label htmlFor="channel_name">{__('Name')}</label>
|
||||
<div className="form-field__prefix">@</div>
|
||||
</fieldset-section>
|
||||
|
||||
<FormField
|
||||
autoFocus={isNewChannel}
|
||||
type="text"
|
||||
name="channel_name"
|
||||
label={__('Name')}
|
||||
placeholder={__('required')}
|
||||
disabled={false}
|
||||
value={params.name}
|
||||
placeholder={__('MyAwesomeChannel')}
|
||||
value={params.name || channelName}
|
||||
error={nameError}
|
||||
disabled={!isNewChannel}
|
||||
onChange={e => setParams({ ...params, name: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
</fieldset-group>
|
||||
{!isNewChannel && <span className="form-field__help">{__('This field cannot be changed.')}</span>}
|
||||
|
||||
<FormField
|
||||
type="text"
|
||||
name="channel_title2"
|
||||
label={__('Title')}
|
||||
placeholder={__('My Awesome Channel')}
|
||||
value={params.title}
|
||||
onChange={e => setParams({ ...params, title: e.target.value })}
|
||||
/>
|
||||
<FormField
|
||||
type="markdown"
|
||||
name="content_description2"
|
||||
label={__('Description')}
|
||||
placeholder={__('Description of your content')}
|
||||
value={params.description}
|
||||
onChange={text => setParams({ ...params, description: text })}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_DESCRIPTION}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Card
|
||||
body={
|
||||
<FormField
|
||||
className="form-field--price-amount"
|
||||
type="number"
|
||||
|
@ -230,49 +285,14 @@ function ChannelForm(props: Props) {
|
|||
disabled={false}
|
||||
onChange={event => handleBidChange(parseFloat(event.target.value))}
|
||||
placeholder={0.1}
|
||||
helper={__('Increasing your deposit can help your channel be discovered more easily.')}
|
||||
/>
|
||||
<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
|
||||
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 })}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_DESCRIPTION}
|
||||
/>
|
||||
<label>{__('Tags')}</label>
|
||||
<div className="tags__border">
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Card
|
||||
body={
|
||||
<TagsSearch
|
||||
suggestMature
|
||||
disableAutoFocus
|
||||
|
@ -293,8 +313,43 @@ function ChannelForm(props: Props) {
|
|||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={'section__actions'}>
|
||||
}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Card
|
||||
body={
|
||||
<>
|
||||
<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 })}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
||||
<Card
|
||||
className="card--after-tabs"
|
||||
actions={
|
||||
<>
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
label={creatingChannel || updatingChannel ? __('Submitting') : __('Submit')}
|
||||
|
@ -306,11 +361,19 @@ function ChannelForm(props: Props) {
|
|||
<ErrorText>{updateError || createError}</ErrorText>
|
||||
) : (
|
||||
<p className="help">
|
||||
{__('After submitting, you will not see the changes immediately. Please check back in a few minutes.')}
|
||||
{__(
|
||||
'After submitting, you will not see the changes immediately. Please check back in a few minutes.'
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
{!isNewChannel && (
|
||||
<div className="section__actions">
|
||||
<ClaimAbandonButton uri={uri} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import classnames from 'classnames';
|
||||
import Gerbil from './gerbil.png';
|
||||
|
@ -25,20 +25,13 @@ function ChannelThumbnail(props: Props) {
|
|||
small = false,
|
||||
allowGifs = false,
|
||||
} = props;
|
||||
const [thumbError, setThumbError] = React.useState(false);
|
||||
const thumbnail = rawThumbnail && rawThumbnail.trim().replace(/^http:\/\//i, 'https://');
|
||||
const thumbnailPreview = rawThumbnailPreview && rawThumbnailPreview.trim().replace(/^http:\/\//i, 'https://');
|
||||
const channelThumbnail = thumbnail || thumbnailPreview;
|
||||
|
||||
const [thumbError, setThumbError] = useState(false);
|
||||
if (channelThumbnail && channelThumbnail.endsWith('gif') && !allowGifs) {
|
||||
return <FreezeframeWrapper src={channelThumbnail} className="channel-thumbnail" />;
|
||||
}
|
||||
|
||||
const showThumb = (!obscure && !!thumbnail) || thumbnailPreview;
|
||||
|
||||
// Generate a random color class based on the first letter of the channel name
|
||||
const { channelName } = parseURI(uri);
|
||||
|
||||
let initializer;
|
||||
let colorClassName;
|
||||
if (channelName) {
|
||||
|
@ -47,6 +40,11 @@ function ChannelThumbnail(props: Props) {
|
|||
} else {
|
||||
colorClassName = `channel-thumbnail__default--4`;
|
||||
}
|
||||
|
||||
if (channelThumbnail && channelThumbnail.endsWith('gif') && !allowGifs) {
|
||||
return <FreezeframeWrapper src={channelThumbnail} className="channel-thumbnail" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('channel-thumbnail', className, {
|
||||
|
|
|
@ -12,11 +12,20 @@ type Props = {
|
|||
};
|
||||
|
||||
export default function ClaimAbandonButton(props: Props) {
|
||||
const { doOpenModal, claim, abandonActionCallback, iconSize } = props;
|
||||
const { doOpenModal, claim, abandonActionCallback } = props;
|
||||
|
||||
function abandonClaim() {
|
||||
doOpenModal(MODALS.CONFIRM_CLAIM_REVOKE, { claim: claim, cb: abandonActionCallback });
|
||||
}
|
||||
|
||||
return <Button disabled={!claim} button="alt" iconSize={iconSize} icon={ICONS.DELETE} onClick={abandonClaim} />;
|
||||
return (
|
||||
<Button
|
||||
disabled={!claim}
|
||||
label={__('Delete Channel')}
|
||||
button="alt"
|
||||
iconColor="red"
|
||||
icon={ICONS.DELETE}
|
||||
onClick={abandonClaim}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,9 +15,14 @@ type Props = {
|
|||
accept?: string,
|
||||
error?: string,
|
||||
disabled?: boolean,
|
||||
autoFocus?: boolean,
|
||||
};
|
||||
|
||||
class FileSelector extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
autoFocus: false,
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
// If the form has just been cleared,
|
||||
// clear the file input
|
||||
|
@ -58,7 +63,18 @@ class FileSelector extends React.PureComponent<Props> {
|
|||
input: ?HTMLInputElement;
|
||||
|
||||
render() {
|
||||
const { type, currentPath, label, fileLabel, directoryLabel, placeholder, accept, error, disabled } = this.props;
|
||||
const {
|
||||
type,
|
||||
currentPath,
|
||||
label,
|
||||
fileLabel,
|
||||
directoryLabel,
|
||||
placeholder,
|
||||
accept,
|
||||
error,
|
||||
disabled,
|
||||
autoFocus = false,
|
||||
} = this.props;
|
||||
const buttonLabel = type === 'file' ? fileLabel || __('Choose File') : directoryLabel || __('Choose Directory');
|
||||
const placeHolder = currentPath || placeholder;
|
||||
|
||||
|
@ -74,7 +90,13 @@ class FileSelector extends React.PureComponent<Props> {
|
|||
readOnly="readonly"
|
||||
value={placeHolder || __('Choose a file')}
|
||||
inputButton={
|
||||
<Button button="secondary" disabled={disabled} onClick={this.fileInputButton} label={buttonLabel} />
|
||||
<Button
|
||||
autoFocus={autoFocus}
|
||||
button="secondary"
|
||||
disabled={disabled}
|
||||
onClick={this.fileInputButton}
|
||||
label={buttonLabel}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<input
|
||||
|
|
|
@ -13,6 +13,7 @@ import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
|||
import Tooltip from 'component/common/tooltip';
|
||||
import NavigationButton from 'component/navigationButton';
|
||||
import { LOGO_TITLE } from 'config';
|
||||
import useIsMobile from 'effects/use-is-mobile';
|
||||
// @if TARGET='app'
|
||||
import { remote } from 'electron';
|
||||
import { IS_MAC } from 'component/app/view';
|
||||
|
@ -37,7 +38,11 @@ type Props = {
|
|||
email: ?string,
|
||||
authenticated: boolean,
|
||||
authHeader: boolean,
|
||||
backout: { backFunction: () => void, backTitle: string },
|
||||
backout: {
|
||||
backFunction: () => void,
|
||||
title: string,
|
||||
simpleTitle: string, // Just use the same value as `title` if `title` is already short (~< 10 chars), unless you have a better idea for title overlfow on mobile
|
||||
},
|
||||
syncError: ?string,
|
||||
emailToVerify?: string,
|
||||
signOut: () => void,
|
||||
|
@ -69,7 +74,7 @@ const Header = (props: Props) => {
|
|||
emailToVerify,
|
||||
backout,
|
||||
} = props;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
// on the verify page don't let anyone escape other than by closing the tab to keep session data consistent
|
||||
const isVerifyPage = history.location.pathname.includes(PAGES.AUTH_VERIFY);
|
||||
const isSignUpPage = history.location.pathname.includes(PAGES.AUTH);
|
||||
|
@ -138,9 +143,9 @@ const Header = (props: Props) => {
|
|||
>
|
||||
<div className="header__contents">
|
||||
{!authHeader && backout ? (
|
||||
<div className="header__contents--between">
|
||||
<Button onClick={backout.backFunction} button="link" label={__('Back')} />
|
||||
{backout.backTitle && <h1 className={'card__title'}>{backout.backTitle}</h1>}
|
||||
<div className="card__actions--between">
|
||||
<Button onClick={backout.backFunction} button="link" label={__('Cancel')} icon={ICONS.ARROW_LEFT} />
|
||||
{backout.title && <h1 className={'card__title'}>{isMobile ? backout.simpleTitle : backout.title}</h1>}
|
||||
<Button
|
||||
aria-label={__('Your wallet')}
|
||||
navigate={`/$/${PAGES.WALLET}`}
|
||||
|
|
|
@ -1,133 +1,125 @@
|
|||
// @flow
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { FormField } from 'component/common/form';
|
||||
import React from 'react';
|
||||
import FileSelector from 'component/common/file-selector';
|
||||
import Button from 'component/button';
|
||||
import { SPEECH_URLS } from 'lbry-redux';
|
||||
import uuid from 'uuid/v4';
|
||||
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '../common/tabs';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import { generateThumbnailName } from 'util/generate-thumbnail-name';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
|
||||
const accept = '.png, .jpg, .jpeg, .gif';
|
||||
const SPEECH_READY = 'READY';
|
||||
const SPEECH_UPLOADING = 'UPLOADING';
|
||||
|
||||
const URL_INDEX = 0;
|
||||
|
||||
type Props = {
|
||||
assetName: string,
|
||||
currentValue: ?string,
|
||||
onUpdate: string => void,
|
||||
recommended: string,
|
||||
title: string,
|
||||
onDone?: () => void,
|
||||
};
|
||||
|
||||
function SelectAsset(props: Props) {
|
||||
const { onUpdate, assetName, currentValue, recommended } = props;
|
||||
const [asset, setAsset] = useState(currentValue);
|
||||
const [pathSelected, setPathSelected] = useState('');
|
||||
const [fileSelected, setFileSelected] = useState<any>(null);
|
||||
const [uploadStatus, setUploadStatus] = useState(SPEECH_READY);
|
||||
const [error, setError] = useState();
|
||||
const [tabIndex, setTabIndex] = useState(URL_INDEX);
|
||||
const { onUpdate, onDone, assetName, recommended, title } = props;
|
||||
const [pathSelected, setPathSelected] = React.useState('');
|
||||
const [fileSelected, setFileSelected] = React.useState<any>(null);
|
||||
const [uploadStatus, setUploadStatus] = React.useState(SPEECH_READY);
|
||||
const [useUrl, setUseUrl] = usePersistedState('thumbnail-upload:mode', false);
|
||||
const [url, setUrl] = React.useState('');
|
||||
const [error, setError] = React.useState();
|
||||
|
||||
function doUploadAsset(file) {
|
||||
React.useEffect(() => {
|
||||
if (pathSelected && fileSelected) {
|
||||
doUploadAsset();
|
||||
}
|
||||
}, [pathSelected, fileSelected]);
|
||||
|
||||
function doUploadAsset() {
|
||||
const uploadError = (error = '') => {
|
||||
setError(error);
|
||||
};
|
||||
|
||||
const setUrl = path => {
|
||||
const onSuccess = thumbnailUrl => {
|
||||
setUploadStatus(SPEECH_READY);
|
||||
onUpdate(path);
|
||||
setAsset(path);
|
||||
setTabIndex(URL_INDEX);
|
||||
onUpdate(thumbnailUrl);
|
||||
|
||||
if (onDone) {
|
||||
onDone();
|
||||
}
|
||||
};
|
||||
|
||||
setUploadStatus(SPEECH_UPLOADING);
|
||||
|
||||
const data = new FormData();
|
||||
const name = uuid();
|
||||
const name = generateThumbnailName();
|
||||
data.append('name', name);
|
||||
data.append('file', file);
|
||||
data.append('file', fileSelected);
|
||||
|
||||
return fetch(SPEECH_URLS.SPEECH_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));
|
||||
.then(json => (json.success ? onSuccess(`${json.data.serveUrl}`) : uploadError(json.message)))
|
||||
.catch(err => {
|
||||
uploadError(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<fieldset-section>
|
||||
<Tabs onChange={n => setTabIndex(n)} index={tabIndex}>
|
||||
<TabList className="tabs__list--select-asset">
|
||||
<Tab>{__('Url')}</Tab>
|
||||
<Tab>{__('Upload')}</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Card
|
||||
title={title || __('Choose Image')}
|
||||
actions={
|
||||
<Form onSubmit={onDone}>
|
||||
{error && <div className="error__text">{error}</div>}
|
||||
{useUrl ? (
|
||||
<FormField
|
||||
autoFocus
|
||||
type={'text'}
|
||||
name={'thumbnail'}
|
||||
label={__(assetName + ' ' + recommended)}
|
||||
label={`${assetName} ${recommended}`}
|
||||
placeholder={'https://example.com/image.png'}
|
||||
disabled={false}
|
||||
value={asset}
|
||||
value={url}
|
||||
onChange={e => {
|
||||
setUrl(e.target.value);
|
||||
onUpdate(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div>
|
||||
{error && <div className="error__text">{error}</div>}
|
||||
{!pathSelected && (
|
||||
) : (
|
||||
<FileSelector
|
||||
label={'File to upload'}
|
||||
name={'assetSelector'}
|
||||
disabled={uploadStatus === SPEECH_UPLOADING}
|
||||
label={uploadStatus === SPEECH_UPLOADING ? __('Uploading...') : __('File to upload')}
|
||||
name="assetSelector"
|
||||
currentPath={pathSelected}
|
||||
onFileChosen={file => {
|
||||
if (file.name) {
|
||||
setPathSelected(file.path || file.name);
|
||||
setFileSelected(file);
|
||||
// file.path is undefined in web but available in electron
|
||||
setPathSelected(file.name || file.path);
|
||||
}
|
||||
}}
|
||||
accept={accept}
|
||||
/>
|
||||
)}
|
||||
{pathSelected && (
|
||||
<div>
|
||||
<FormField
|
||||
type={'text'}
|
||||
name={'uploaded_thumbnail'}
|
||||
label={''}
|
||||
placeholder={'https://example.com/image.png'}
|
||||
disabled={false}
|
||||
value={`${pathSelected}`}
|
||||
/>
|
||||
<div>
|
||||
<Button button={'primary'} onClick={() => doUploadAsset(fileSelected)}>
|
||||
Upload
|
||||
</Button>
|
||||
<Button
|
||||
button={'secondary'}
|
||||
onClick={() => {
|
||||
setPathSelected('');
|
||||
setFileSelected(null);
|
||||
setError(null);
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
{uploadStatus}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section__actions">
|
||||
{onDone && (
|
||||
<Button button="primary" type="submit" label={__('Done')} disabled={uploadStatus === SPEECH_UPLOADING} />
|
||||
)}
|
||||
<FormField
|
||||
name="toggle-upload"
|
||||
type="checkbox"
|
||||
label={__('Use a URL instead')}
|
||||
checked={useUrl}
|
||||
onChange={() => setUseUrl(!useUrl)}
|
||||
/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</fieldset-section>
|
||||
</Form>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -80,7 +80,6 @@ export default function YoutubeTransferStatus(props: Props) {
|
|||
return (
|
||||
hasChannels &&
|
||||
!isYoutubeTransferComplete && (
|
||||
<div>
|
||||
<Card
|
||||
title={youtubeChannels.length > 1 ? __('Your YouTube Channels') : __('Your YouTube Channel')}
|
||||
subtitle={
|
||||
|
@ -94,11 +93,7 @@ export default function YoutubeTransferStatus(props: Props) {
|
|||
body={
|
||||
<section>
|
||||
{youtubeChannels.map((channel, index) => {
|
||||
const {
|
||||
lbry_channel_name: channelName,
|
||||
channel_claim_id: claimId,
|
||||
status_token: statusToken,
|
||||
} = channel;
|
||||
const { lbry_channel_name: channelName, channel_claim_id: claimId, status_token: statusToken } = channel;
|
||||
const url = buildURI({ channelName, channelClaimId: claimId });
|
||||
const transferState = getMessage(channel);
|
||||
return (
|
||||
|
@ -161,7 +156,6 @@ export default function YoutubeTransferStatus(props: Props) {
|
|||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,41 +1,31 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Modal } from 'modal/modal';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import SelectAsset from 'component/selectAsset';
|
||||
|
||||
type Props = {
|
||||
closeModal: () => void,
|
||||
currentValue: string,
|
||||
label: string,
|
||||
helptext: string,
|
||||
title: string,
|
||||
helpText: string,
|
||||
onUpdate: string => void,
|
||||
assetName: string,
|
||||
};
|
||||
|
||||
const ModalImageUpload = (props: Props) => {
|
||||
const { closeModal, currentValue, label, helptext, onUpdate } = props;
|
||||
function ModalImageUpload(props: Props) {
|
||||
const { closeModal, currentValue, title, assetName, helpText, onUpdate } = props;
|
||||
|
||||
return (
|
||||
<Modal isOpen type="card" onAborted={closeModal}>
|
||||
<Card
|
||||
title={__(label)}
|
||||
body={
|
||||
<Modal isOpen type="card" onAborted={closeModal} contentLabel={title}>
|
||||
<SelectAsset
|
||||
onUpdate={v => onUpdate(v)}
|
||||
currentValue={currentValue}
|
||||
assetName={label}
|
||||
recommended={__(helptext)}
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
<div className="card__actions">
|
||||
<Button button="primary" label={__('Done')} onClick={closeModal} />
|
||||
</div>
|
||||
}
|
||||
assetName={assetName}
|
||||
recommended={helpText}
|
||||
onDone={closeModal}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default ModalImageUpload;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import Page from 'component/page';
|
||||
|
@ -8,7 +8,7 @@ import SubscribeButton from 'component/subscribeButton';
|
|||
import BlockButton from 'component/blockButton';
|
||||
import ShareButton from 'component/shareButton';
|
||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||
import { withRouter } from 'react-router';
|
||||
import { useHistory } from 'react-router';
|
||||
import Button from 'component/button';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import ChannelContent from 'component/channelContent';
|
||||
|
@ -26,6 +26,7 @@ import ClaimSupportButton from 'component/claimSupportButton';
|
|||
const PAGE_VIEW_QUERY = `view`;
|
||||
const ABOUT_PAGE = `about`;
|
||||
const DISCUSSION_PAGE = `discussion`;
|
||||
const EDIT_PAGE = 'edit';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
@ -34,8 +35,6 @@ type Props = {
|
|||
cover: ?string,
|
||||
thumbnail: ?string,
|
||||
page: number,
|
||||
location: { search: string },
|
||||
history: { push: string => void },
|
||||
match: { params: { attribute: ?string } },
|
||||
channelIsMine: boolean,
|
||||
isSubscribed: boolean,
|
||||
|
@ -55,8 +54,6 @@ function ChannelPage(props: Props) {
|
|||
claim,
|
||||
title,
|
||||
cover,
|
||||
history,
|
||||
location,
|
||||
page,
|
||||
channelIsMine,
|
||||
isSubscribed,
|
||||
|
@ -66,16 +63,27 @@ function ChannelPage(props: Props) {
|
|||
subCount,
|
||||
pending,
|
||||
} = props;
|
||||
|
||||
const { channelName } = parseURI(uri);
|
||||
const { search } = location;
|
||||
const {
|
||||
push,
|
||||
goBack,
|
||||
location: { search },
|
||||
} = useHistory();
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined;
|
||||
const editInUrl = urlParams.get(PAGE_VIEW_QUERY) === EDIT_PAGE;
|
||||
const [editing, setEditing] = React.useState(editInUrl);
|
||||
const [lastYtSyncDate, setLastYtSyncDate] = React.useState();
|
||||
const { channelName } = parseURI(uri);
|
||||
const { permanent_url: permanentUrl } = claim;
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [lastYtSyncDate, setLastYtSyncDate] = useState();
|
||||
const claimId = claim.claim_id;
|
||||
const formattedSubCount = Number(subCount).toLocaleString();
|
||||
let channelIsBlackListed = false;
|
||||
|
||||
if (claim && blackListedOutpoints) {
|
||||
channelIsBlackListed = blackListedOutpoints.some(
|
||||
outpoint => outpoint.txid === claim.txid && outpoint.nout === claim.nout
|
||||
);
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -93,14 +101,34 @@ function ChannelPage(props: Props) {
|
|||
} else {
|
||||
search += `${PAGE_VIEW_QUERY}=${DISCUSSION_PAGE}`;
|
||||
}
|
||||
history.push(`${url}${search}`);
|
||||
|
||||
push(`${url}${search}`);
|
||||
}
|
||||
|
||||
function onDone() {
|
||||
setEditing(false);
|
||||
goBack();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if (!channelIsMine && editing) {
|
||||
setEditing(false);
|
||||
}
|
||||
|
||||
if (channelIsMine && editing) {
|
||||
push(`?${PAGE_VIEW_QUERY}=${EDIT_PAGE}`);
|
||||
}
|
||||
}, [channelIsMine, editing, push]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (currentView === EDIT_PAGE) {
|
||||
setEditing(true);
|
||||
} else {
|
||||
setEditing(false);
|
||||
}
|
||||
}, [currentView, setEditing]);
|
||||
|
||||
React.useEffect(() => {
|
||||
Lbryio.call('yt', 'get_youtuber', { channel_claim_id: claimId }).then(response => {
|
||||
if (response.is_verified_youtuber) {
|
||||
setLastYtSyncDate(response.last_synced);
|
||||
|
@ -108,31 +136,20 @@ function ChannelPage(props: Props) {
|
|||
});
|
||||
}, [claimId]);
|
||||
|
||||
let channelIsBlackListed = false;
|
||||
|
||||
if (claim && blackListedOutpoints) {
|
||||
channelIsBlackListed = blackListedOutpoints.some(
|
||||
outpoint => outpoint.txid === claim.txid && outpoint.nout === claim.nout
|
||||
);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchSubCount(claimId);
|
||||
}, [uri, fetchSubCount, claimId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!channelIsMine && editing) {
|
||||
setEditing(false);
|
||||
}
|
||||
}, [channelIsMine, editing]);
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<Page
|
||||
noFooter
|
||||
noSideNavigation={editing}
|
||||
title={__('Edit Channel')}
|
||||
backout={{ backFunction: onDone, backTitle: __('Edit Channel') }}
|
||||
backout={{
|
||||
backFunction: onDone,
|
||||
title: __('Editing @%channel%', { channel: channelName }),
|
||||
simpleTitle: __('Editing'),
|
||||
}}
|
||||
>
|
||||
<ChannelEdit uri={uri} onDone={onDone} />
|
||||
</Page>
|
||||
|
@ -219,4 +236,4 @@ function ChannelPage(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default withRouter(ChannelPage);
|
||||
export default ChannelPage;
|
||||
|
|
|
@ -30,6 +30,7 @@ export default function ChannelsPage(props: Props) {
|
|||
|
||||
return (
|
||||
<Page>
|
||||
<div className="card-stack">
|
||||
{hasYoutubeChannels && <YoutubeTransferStatus hideChannelLink />}
|
||||
|
||||
{channelUrls && Boolean(channelUrls.length) && (
|
||||
|
@ -49,6 +50,8 @@ export default function ChannelsPage(props: Props) {
|
|||
body={<ClaimList isCardBody loading={fetchingChannels} uris={channelUrls} />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!(channelUrls && channelUrls.length) && (
|
||||
<React.Fragment>
|
||||
{!fetchingChannels ? (
|
||||
|
|
|
@ -53,6 +53,11 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.card--after-tabs {
|
||||
@extend .card;
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
|
||||
.card__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -107,6 +112,8 @@
|
|||
|
||||
.card__actions--between {
|
||||
@include between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card__actions--center {
|
||||
|
@ -161,15 +168,21 @@
|
|||
align-items: center;
|
||||
font-size: var(--font-title);
|
||||
font-weight: var(--font-weight-light);
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
|
||||
/* .badge rule inherited from file page prices, should be refactored */
|
||||
.badge {
|
||||
float: right;
|
||||
margin-left: var(--spacing-s);
|
||||
margin-top: 8px; // should be flex'd, but don't blame me! I just moved it down 3px
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
font-size: var(--font-body);
|
||||
}
|
||||
}
|
||||
|
||||
.card__title-actions {
|
||||
|
|
|
@ -10,11 +10,6 @@ $metadata-z-index: 1;
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
.channel-cover--edit {
|
||||
@extend .channel-cover;
|
||||
padding-top: 4rem;
|
||||
}
|
||||
|
||||
.channel-cover__custom {
|
||||
z-index: $cover-z-index;
|
||||
align-self: flex-start;
|
||||
|
@ -25,9 +20,12 @@ $metadata-z-index: 1;
|
|||
.channel-cover,
|
||||
.channel-cover__custom {
|
||||
min-height: var(--cover-photo-height);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-top-left-radius: var(--card-radius);
|
||||
border-top-right-radius: var(--card-radius);
|
||||
border: 1px sold var(--color-border);
|
||||
border-bottom: none;
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
// Yikes
|
||||
|
@ -77,7 +75,8 @@ $metadata-z-index: 1;
|
|||
box-shadow: var(--card-box-shadow);
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
display: none;
|
||||
top: 0;
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,7 +123,7 @@ $metadata-z-index: 1;
|
|||
z-index: $metadata-z-index;
|
||||
// Jump over the thumbnail photo because it is absolutely positioned
|
||||
// Then add normal page spacing, _then_ add the actual padding
|
||||
padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-l));
|
||||
padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-xl));
|
||||
padding-right: var(--spacing-m);
|
||||
padding-bottom: var(--spacing-m);
|
||||
margin-top: var(--spacing-xl);
|
||||
|
@ -133,7 +132,7 @@ $metadata-z-index: 1;
|
|||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding-left: var(--spacing-m);
|
||||
margin-top: 0;
|
||||
margin-top: calc(var(--channel-thumbnail-width) + var(--spacing-l));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,22 +190,27 @@ $metadata-z-index: 1;
|
|||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
flex-direction: column;
|
||||
|
||||
> * {
|
||||
margin-right: 0;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
margin-left: auto;
|
||||
|
||||
// Needed for specificity above
|
||||
&:not(:last-child) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channel__edit-thumb {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: var(--spacing-l);
|
||||
margin-top: calc(var(--spacing-m) * 7);
|
||||
z-index: $metadata-z-index;
|
||||
flex-wrap: wrap;
|
||||
font-size: var(--font-base);
|
||||
|
||||
> * {
|
||||
padding: 0 var(--spacing-xs);
|
||||
|
@ -217,45 +221,12 @@ $metadata-z-index: 1;
|
|||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
> * {
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
margin-top: 0;
|
||||
|
||||
.channel__edit-cover {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: var(--spacing-m);
|
||||
margin-top: var(--spacing-m);
|
||||
z-index: $metadata-z-index;
|
||||
flex-wrap: wrap;
|
||||
font-size: var(--font-base);
|
||||
|
||||
> * {
|
||||
padding: 0 var(--spacing-xs);
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: var(--spacing-m);
|
||||
.button {
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
> * {
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channel__uri-preview {
|
||||
position: absolute;
|
||||
top: -2rem;
|
||||
left: 0;
|
||||
z-index: $metadata-z-index;
|
||||
font-size: var(--font-base);
|
||||
color: var(--color-text-subtitle);
|
||||
}
|
||||
|
||||
.channel-name--inline {
|
||||
|
|
|
@ -87,7 +87,7 @@ fieldset-group {
|
|||
border-color: var(--color-input-border);
|
||||
color: var(--color-text-help);
|
||||
background-color: var(--color-input-bg);
|
||||
border-right: 1px solid var(--color-text-help);
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,8 +133,6 @@ fieldset-group {
|
|||
|
||||
.form-field__help {
|
||||
@extend .help;
|
||||
margin-top: var(--spacing-xxs);
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.form-field__help + .checkbox,
|
||||
|
|
|
@ -43,12 +43,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.header__contents--between {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header__navigation {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
.main-wrapper {
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.main-wrapper--mac {
|
||||
|
@ -25,6 +27,8 @@
|
|||
.main {
|
||||
position: relative;
|
||||
width: calc(100% - var(--side-nav-width) - var(--spacing-l));
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
width: 100%;
|
||||
|
|
|
@ -47,12 +47,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tags__border {
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--color-input-border);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
@extend .badge;
|
||||
@extend .badge--tag;
|
||||
|
|
|
@ -109,9 +109,11 @@
|
|||
flex-wrap: wrap;
|
||||
|
||||
> * {
|
||||
&:not(:only-of-type) {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button--primary,
|
||||
.button ~ .button--link {
|
||||
|
|
|
@ -15,12 +15,14 @@
|
|||
}
|
||||
|
||||
.tabs__list--channel-page {
|
||||
padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-l));
|
||||
padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-xl));
|
||||
padding-right: var(--spacing-m);
|
||||
margin-bottom: var(--spacing-l);
|
||||
height: 4rem;
|
||||
border-bottom-left-radius: var(--card-radius);
|
||||
border-bottom-right-radius: var(--card-radius);
|
||||
border: 1px solid var(--color-border);
|
||||
border-top: none;
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding-left: var(--spacing-m);
|
||||
|
@ -59,6 +61,10 @@
|
|||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-xsmall) {
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
.tab__divider {
|
||||
|
|
7
ui/util/generate-thumbnail-name.js
Normal file
7
ui/util/generate-thumbnail-name.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
// @flow
|
||||
export const generateThumbnailName = (): string => {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < 24; i += 1) text += possible.charAt(Math.floor(Math.random() * 62));
|
||||
return text;
|
||||
};
|
|
@ -957,10 +957,10 @@
|
|||
prop-types "^15.6.2"
|
||||
scheduler "^0.18.0"
|
||||
|
||||
"@lbry/components@^4.2.2":
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-4.2.2.tgz#023b8224e180b69cd8b5d77242441742bca26d0f"
|
||||
integrity sha512-CziwuALDiv/DXT5zwkVK8cfF914WyOqKg8GkqIk/f9vc7VXZx9OlRfBadnpGDrezrjjHn7onwOreQrwzB5PcNA==
|
||||
"@lbry/components@^4.2.5":
|
||||
version "4.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-4.2.5.tgz#21d2cad296c015c6300727e706a1456e72aae600"
|
||||
integrity sha512-UmE4nkvTQnZX4obOiR650apXarRmvXW+Ieezu/M4LZNKL/CRmHJIRrqcSMtPugDf/Ijukx/4oqrq2M8Ww0sgyw==
|
||||
|
||||
"@mapbox/hast-util-table-cell-style@^0.1.3":
|
||||
version "0.1.3"
|
||||
|
|
Loading…
Reference in a new issue