redesign channel page
This commit is contained in:
parent
37af00b01e
commit
b2decaac73
46 changed files with 851 additions and 483 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"parser": "babel-eslint",
|
||||
"extends": ["standard", "standard-jsx", "plugin:flowtype/recommended"],
|
||||
"plugins": ["flowtype", "import"],
|
||||
"extends": ["standard", "standard-jsx", "plugin:react/recommended", "plugin:flowtype/recommended"],
|
||||
"plugins": ["flowtype", "import", "react-hooks"],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"webpack": {
|
||||
|
@ -22,20 +22,21 @@
|
|||
"IS_WEB": true
|
||||
},
|
||||
"rules": {
|
||||
"no-multi-spaces": 0,
|
||||
"new-cap": 0,
|
||||
"prefer-promise-reject-errors": 0,
|
||||
"no-unused-vars": 0,
|
||||
"standard/object-curly-even-spacing": 0,
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"handle-callback-err": 0,
|
||||
"one-var": 0,
|
||||
"object-curly-spacing": 0,
|
||||
"jsx-quotes": ["error", "prefer-double"],
|
||||
"new-cap": 0,
|
||||
"no-multi-spaces": 0,
|
||||
"no-redeclare": 0,
|
||||
"no-return-await": 0,
|
||||
"standard/no-callback-literal": 0,
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"object-curly-spacing": 0,
|
||||
"one-var": 0,
|
||||
"prefer-promise-reject-errors": 0,
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"jsx-quotes": ["error", "prefer-double"],
|
||||
"standard/object-curly-even-spacing": 0,
|
||||
"standard/no-callback-literal": 0,
|
||||
"semi": [
|
||||
"error",
|
||||
"always",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"linters": {
|
||||
"src/**/*.{js,jsx,scss,json}": ["prettier --write", "git add"],
|
||||
"src/**/*.{js,jsx}": ["eslint --fix", "flow focus-check --color always", "git add"]
|
||||
"src/**/*.{js,jsx}": ["eslint", "flow focus-check --color always", "git add"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100,
|
||||
"printWidth": 120,
|
||||
"singleQuote": true
|
||||
}
|
|
@ -40,6 +40,8 @@
|
|||
"postinstall": "electron-builder install-app-deps && node build/downloadDaemon.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reach/rect": "^0.2.1",
|
||||
"@reach/tabs": "^0.1.5",
|
||||
"electron-dl": "^1.11.0",
|
||||
"electron-log": "^2.2.12",
|
||||
"electron-updater": "^4.0.6",
|
||||
|
@ -104,6 +106,7 @@
|
|||
"eslint-plugin-prettier": "^2.6.0",
|
||||
"eslint-plugin-promise": "^4.0.1",
|
||||
"eslint-plugin-react": "^7.7.0",
|
||||
"eslint-plugin-react-hooks": "^1.6.0",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"flow-bin": "^0.97.0",
|
||||
"flow-typed": "^2.3.0",
|
||||
|
@ -114,7 +117,7 @@
|
|||
"jsmediatags": "^3.8.1",
|
||||
"json-loader": "^0.5.4",
|
||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||
"lbry-redux": "lbryio/lbry-redux#cc42856676541120b088e4228c04246ba8ff3274",
|
||||
"lbry-redux": "lbryio/lbry-redux#459bea2257d61003e591daf169fefe9624522680",
|
||||
"lbryinc": "lbryio/lbryinc#9665f2d1c818f1a86b2e5daab642f6879746f25f",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
|
@ -185,7 +188,7 @@
|
|||
"yarn": "^1.3"
|
||||
},
|
||||
"lbrySettings": {
|
||||
"lbrynetDaemonVersion": "0.37.0rc3",
|
||||
"lbrynetDaemonVersion": "0.37.0rc4",
|
||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
||||
"lbrynetDaemonDir": "static/daemon",
|
||||
"lbrynetDaemonFileName": "lbrynet"
|
||||
|
|
|
@ -10,7 +10,7 @@ module.exports = ({ file, options, env }) => {
|
|||
parser: file.extname === '.sss' ? 'sugarss' : false,
|
||||
plugins: {
|
||||
'postcss-import': { root: file.dirname },
|
||||
'cssnano': env === 'production' ? options.cssnano : false
|
||||
}
|
||||
cssnano: env === 'production' ? options.cssnano : false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
14
src/ui/component/channelAbout/index.js
Normal file
14
src/ui/component/channelAbout/index.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectMetadataItemForUri } from 'lbry-redux';
|
||||
import ChannelAbout from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
description: makeSelectMetadataItemForUri(props.uri, 'description')(state),
|
||||
website: makeSelectMetadataItemForUri(props.uri, 'website_url')(state),
|
||||
email: makeSelectMetadataItemForUri(props.uri, 'email')(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
null
|
||||
)(ChannelAbout);
|
38
src/ui/component/channelAbout/view.jsx
Normal file
38
src/ui/component/channelAbout/view.jsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
// @flow
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
type Props = {
|
||||
description: ?string,
|
||||
email: ?string,
|
||||
website: ?string,
|
||||
};
|
||||
|
||||
function ChannelContent(props: Props) {
|
||||
const { description, email, website } = props;
|
||||
const showAbout = description || email || website;
|
||||
|
||||
return (
|
||||
<section>
|
||||
{!showAbout && <h2 className="empty">{__('Nothing here yet')}</h2>}
|
||||
{showAbout && (
|
||||
<Fragment>
|
||||
{description && <div className="media__info-text">{description}</div>}
|
||||
{email && (
|
||||
<Fragment>
|
||||
<div className="media__info-title">{__('Contact')}</div>
|
||||
<div className="media__info-text">{email}</div>
|
||||
</Fragment>
|
||||
)}
|
||||
{website && (
|
||||
<Fragment>
|
||||
<div className="media__info-title">{__('Site')}</div>
|
||||
<div className="media__info-text">{website}</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChannelContent;
|
26
src/ui/component/channelContent/index.js
Normal file
26
src/ui/component/channelContent/index.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doFetchClaimsByChannel } from 'redux/actions/content';
|
||||
import { PAGE_SIZE } from 'constants/claim';
|
||||
import {
|
||||
makeSelectClaimsInChannelForCurrentPageState,
|
||||
makeSelectFetchingChannelClaims,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectTotalPagesForChannel,
|
||||
} from 'lbry-redux';
|
||||
import ChannelPage from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claimsInChannel: makeSelectClaimsInChannelForCurrentPageState(props.uri)(state),
|
||||
fetching: makeSelectFetchingChannelClaims(props.uri)(state),
|
||||
totalPages: makeSelectTotalPagesForChannel(props.uri, PAGE_SIZE)(state),
|
||||
channelIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchClaims: (uri, page) => dispatch(doFetchClaimsByChannel(uri, page)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(ChannelPage);
|
47
src/ui/component/channelContent/view.jsx
Normal file
47
src/ui/component/channelContent/view.jsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
// @flow
|
||||
import React, { Fragment } from 'react';
|
||||
import FileList from 'component/fileList';
|
||||
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import Paginate from 'component/common/paginate';
|
||||
import Spinner from 'component/spinner';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
totalPages: number,
|
||||
fetching: boolean,
|
||||
params: { page: number },
|
||||
claimsInChannel: Array<StreamClaim>,
|
||||
channelIsMine: boolean,
|
||||
fetchClaims: (string, number) => void,
|
||||
location: UrlLocation,
|
||||
};
|
||||
|
||||
function ChannelContent(props: Props) {
|
||||
const { uri, fetching, claimsInChannel, totalPages, channelIsMine, fetchClaims } = props;
|
||||
const hasContent = Boolean(claimsInChannel && claimsInChannel.length);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{fetching && !hasContent && (
|
||||
<section className="main--empty">
|
||||
<Spinner delayed />
|
||||
</section>
|
||||
)}
|
||||
|
||||
{!fetching && !hasContent && <h2 className="empty">{__("This channel hasn't uploaded anything.")}</h2>}
|
||||
|
||||
{!channelIsMine && <HiddenNsfwClaims className="card__content help" uri={uri} />}
|
||||
|
||||
{hasContent && <FileList sortByHeight hideFilter fileInfos={claimsInChannel} />}
|
||||
|
||||
<Paginate
|
||||
onPageChange={page => fetchClaims(uri, page)}
|
||||
totalPages={totalPages}
|
||||
loading={fetching && !hasContent}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(ChannelContent);
|
12
src/ui/component/channelThumbnail/index.js
Normal file
12
src/ui/component/channelThumbnail/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectThumbnailForUri } from 'lbry-redux';
|
||||
import ChannelThumbnail from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
null
|
||||
)(ChannelThumbnail);
|
17
src/ui/component/channelThumbnail/view.jsx
Normal file
17
src/ui/component/channelThumbnail/view.jsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
thumbnail: ?string,
|
||||
};
|
||||
|
||||
function ChannelThumbnail(props: Props) {
|
||||
const { thumbnail } = props;
|
||||
return (
|
||||
<div className="channel__thumbnail">
|
||||
{thumbnail && <img className="channel__thumbnail--custom" src={thumbnail} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChannelThumbnail;
|
81
src/ui/component/common/paginate.jsx
Normal file
81
src/ui/component/common/paginate.jsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router';
|
||||
import { Form, FormField } from 'component/common/form';
|
||||
import ReactPaginate from 'react-paginate';
|
||||
|
||||
const PAGINATE_PARAM = 'page';
|
||||
const ENTER_KEY_CODE = 13;
|
||||
|
||||
type Props = {
|
||||
loading: boolean,
|
||||
totalPages: number,
|
||||
location: { search: string },
|
||||
history: { push: string => void },
|
||||
onPageChange?: number => void,
|
||||
};
|
||||
|
||||
function Paginate(props: Props) {
|
||||
const { totalPages, loading, location, history, onPageChange } = props;
|
||||
const { search } = location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const currentPage = Number(urlParams.get(PAGINATE_PARAM)) || 1;
|
||||
|
||||
function handleChangePage(newPageNumber: number) {
|
||||
if (onPageChange) {
|
||||
onPageChange(newPageNumber);
|
||||
}
|
||||
|
||||
if (currentPage !== newPageNumber) {
|
||||
history.push(`?${PAGINATE_PARAM}=${newPageNumber}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePaginateKeyUp(e: SyntheticKeyboardEvent<*>) {
|
||||
const newPage = Number(e.currentTarget.value);
|
||||
const isEnterKey = e.keyCode === ENTER_KEY_CODE;
|
||||
|
||||
if (newPage && isEnterKey && newPage > 0 && newPage <= totalPages) {
|
||||
handleChangePage(newPage);
|
||||
}
|
||||
}
|
||||
|
||||
if (totalPages <= 1 || loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<fieldset-group class="fieldset-group--smushed fieldgroup--paginate">
|
||||
<fieldset-section>
|
||||
<ReactPaginate
|
||||
pageCount={totalPages}
|
||||
pageRangeDisplayed={2}
|
||||
previousLabel="‹"
|
||||
nextLabel="›"
|
||||
activeClassName="pagination__item--selected"
|
||||
pageClassName="pagination__item"
|
||||
previousClassName="pagination__item pagination__item--previous"
|
||||
nextClassName="pagination__item pagination__item--next"
|
||||
breakClassName="pagination__item pagination__item--break"
|
||||
marginPagesDisplayed={2}
|
||||
onPageChange={e => handleChangePage(e.selected + 1)}
|
||||
forcePage={currentPage - 1}
|
||||
initialPage={currentPage - 1}
|
||||
containerClassName="pagination"
|
||||
/>
|
||||
</fieldset-section>
|
||||
|
||||
<FormField
|
||||
className="paginate-channel"
|
||||
onKeyUp={handlePaginateKeyUp}
|
||||
label={__('Go to page:')}
|
||||
type="text"
|
||||
name="paginate-file"
|
||||
/>
|
||||
</fieldset-group>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(Paginate);
|
130
src/ui/component/common/tabs.jsx
Normal file
130
src/ui/component/common/tabs.jsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
// @flow
|
||||
import React, { Fragment, useState, useRef, useContext, useLayoutEffect, createContext } from 'react';
|
||||
import {
|
||||
Tabs as ReachTabs,
|
||||
Tab as ReachTab,
|
||||
TabList as ReachTabList,
|
||||
TabPanels as ReachTabPanels,
|
||||
TabPanel as ReachTabPanel,
|
||||
} from '@reach/tabs';
|
||||
import classnames from 'classnames';
|
||||
import { useRect } from '@reach/rect';
|
||||
|
||||
// Tabs are a compound component
|
||||
// The components are used individually, but they will still interact and share state
|
||||
// When using, at a minimum you must arrange the components in this pattern
|
||||
// When the <Tab> at index 0 is active, the TabPanel at index 0 will be displayed
|
||||
//
|
||||
// <Tabs onChange={...} index={...}>
|
||||
// <TabList>
|
||||
// <Tab>Tab label 1</Tab>
|
||||
// <Tab>Tab label 2</Tab>
|
||||
// ...
|
||||
// </TabList>
|
||||
// <TabPanels>
|
||||
// <TabPanel>Content for Tab 1</TabPanel>
|
||||
// <TabPanel>Content for Tab 2</TabPanel>
|
||||
// ...
|
||||
// </TabPanels>
|
||||
// </Tabs>
|
||||
//
|
||||
// the base @reach/tabs components handle all the focus/accessibilty labels
|
||||
// We're just adding some styling
|
||||
|
||||
type TabsProps = {
|
||||
index?: number,
|
||||
onChange?: number => void,
|
||||
children: Array<React$Node>,
|
||||
};
|
||||
|
||||
// Use context so child TabPanels can set the active tab, which is kept in Tabs' state
|
||||
const AnimatedContext = createContext<any>();
|
||||
|
||||
function Tabs(props: TabsProps) {
|
||||
// Store the position of the selected Tab so we can animate the "active" bar to its position
|
||||
const [selectedRect, setSelectedRect] = useState(null);
|
||||
|
||||
// Create a ref of the parent element so we can measure the relative "left" for the bar for the child Tab's
|
||||
const tabsRef = useRef();
|
||||
const tabsRect = useRect(tabsRef);
|
||||
|
||||
const tabLabels = props.children[0];
|
||||
const tabContent = props.children[1];
|
||||
|
||||
return (
|
||||
<AnimatedContext.Provider value={setSelectedRect}>
|
||||
<ReachTabs className="tabs" {...props} ref={tabsRef}>
|
||||
{tabLabels}
|
||||
|
||||
<div
|
||||
className="tab__divider"
|
||||
style={{
|
||||
left: selectedRect && selectedRect.left - tabsRect.left,
|
||||
width: selectedRect && selectedRect.width,
|
||||
}}
|
||||
/>
|
||||
|
||||
{tabContent}
|
||||
</ReachTabs>
|
||||
</AnimatedContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// The wrapper for the list of tab labels that users can click
|
||||
type TabListProps = {
|
||||
className?: string,
|
||||
};
|
||||
function TabList(props: TabListProps) {
|
||||
const { className, ...rest } = props;
|
||||
return <ReachTabList className={classnames('tabs__list', className)} {...rest} />;
|
||||
}
|
||||
|
||||
//
|
||||
// The links that users click
|
||||
// Accesses `setSelectedRect` from context to set itself as active if needed
|
||||
// Flow doesn't understand we don't have to pass it in ourselves
|
||||
type TabProps = {
|
||||
isSelected?: Boolean,
|
||||
};
|
||||
function Tab(props: TabProps) {
|
||||
// @reach/tabs provides an `isSelected` prop
|
||||
// We could also useContext to read it manually
|
||||
const { isSelected } = props;
|
||||
|
||||
// Each tab measures itself
|
||||
const ref = useRef();
|
||||
const rect = useRect(ref, isSelected);
|
||||
|
||||
// and calls up to the parent when it becomes selected
|
||||
// we useLayoutEffect to avoid flicker
|
||||
const setSelectedRect = useContext(AnimatedContext);
|
||||
useLayoutEffect(() => {
|
||||
if (isSelected) setSelectedRect(rect);
|
||||
}, [isSelected, rect, setSelectedRect]);
|
||||
|
||||
return <ReachTab ref={ref} {...props} className="tab" />;
|
||||
}
|
||||
|
||||
//
|
||||
// The wrapper for TabPanel
|
||||
type TabPanelsProps = {
|
||||
header?: React$Node,
|
||||
};
|
||||
function TabPanels(props: TabPanelsProps) {
|
||||
const { header, ...rest } = props;
|
||||
return (
|
||||
<Fragment>
|
||||
{header}
|
||||
<ReachTabPanels {...rest} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// The wrapper for content when it's associated Tab is selected
|
||||
function TabPanel(props: any) {
|
||||
return <ReachTabPanel className="tab__panel" {...props} />;
|
||||
}
|
||||
|
||||
export { Tabs, TabList, Tab, TabPanels, TabPanel };
|
|
@ -10,12 +10,12 @@ type Props = {
|
|||
|
||||
export default (props: Props) => {
|
||||
const { numberOfNsfwClaims, obscureNsfw, className } = props;
|
||||
|
||||
return (
|
||||
obscureNsfw &&
|
||||
Boolean(numberOfNsfwClaims) && (
|
||||
<div className={className || 'help'}>
|
||||
{numberOfNsfwClaims} {numberOfNsfwClaims > 1 ? __('files') : __('file')}{' '}
|
||||
{__('hidden due to your')}{' '}
|
||||
{numberOfNsfwClaims} {numberOfNsfwClaims > 1 ? __('files') : __('file')} {__('hidden due to your')}{' '}
|
||||
<Button button="link" navigate="/$/settings" label={__('content viewing preferences')} />.
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { Form, FormField } from 'component/common/form';
|
||||
import ReactPaginate from 'react-paginate';
|
||||
import NavigationHistoryItem from 'component/navigationHistoryItem';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import Paginate from 'component/common/paginate';
|
||||
|
||||
type HistoryItem = {
|
||||
uri: string,
|
||||
|
@ -13,11 +11,8 @@ type HistoryItem = {
|
|||
|
||||
type Props = {
|
||||
historyItems: Array<HistoryItem>,
|
||||
page: number,
|
||||
pageCount: number,
|
||||
clearHistoryUri: string => void,
|
||||
params: { page: number },
|
||||
history: { push: string => void },
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -52,24 +47,6 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
|
|||
});
|
||||
}
|
||||
|
||||
changePage(pageNumber: number) {
|
||||
const { history } = this.props;
|
||||
history.push(`?page=${pageNumber}`);
|
||||
}
|
||||
|
||||
paginate(e: SyntheticKeyboardEvent<*>) {
|
||||
const pageFromInput = Number(e.currentTarget.value);
|
||||
if (
|
||||
pageFromInput &&
|
||||
e.keyCode === 13 &&
|
||||
!Number.isNaN(pageFromInput) &&
|
||||
pageFromInput > 0 &&
|
||||
pageFromInput <= this.props.pageCount
|
||||
) {
|
||||
this.changePage(pageFromInput);
|
||||
}
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
const { historyItems } = this.props;
|
||||
const newSelectedState = {};
|
||||
|
@ -94,26 +71,20 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { historyItems = [], page, pageCount } = this.props;
|
||||
const { historyItems = [], pageCount } = this.props;
|
||||
const { itemsSelected } = this.state;
|
||||
const allSelected = Object.keys(itemsSelected).length === history.length;
|
||||
const allSelected = Object.keys(itemsSelected).length === historyItems.length;
|
||||
const selectHandler = allSelected ? this.unselectAll : this.selectAll;
|
||||
|
||||
return history.length ? (
|
||||
return historyItems.length ? (
|
||||
<React.Fragment>
|
||||
<div className="card__actions card__actions--between">
|
||||
{Object.keys(itemsSelected).length ? (
|
||||
<Button button="link" label={__('Delete')} onClick={this.removeSelected} />
|
||||
) : (
|
||||
<span>
|
||||
{/* Using an empty span so spacing stays the same if the button isn't rendered */}
|
||||
</span>
|
||||
<span>{/* Using an empty span so spacing stays the same if the button isn't rendered */}</span>
|
||||
)}
|
||||
<Button
|
||||
button="link"
|
||||
label={allSelected ? __('Cancel') : __('Select All')}
|
||||
onClick={selectHandler}
|
||||
/>
|
||||
<Button button="link" label={allSelected ? __('Cancel') : __('Select All')} onClick={selectHandler} />
|
||||
</div>
|
||||
{!!historyItems.length && (
|
||||
<section className="card__content item-list">
|
||||
|
@ -130,47 +101,14 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
|
|||
))}
|
||||
</section>
|
||||
)}
|
||||
{pageCount > 1 && (
|
||||
<Form>
|
||||
<fieldset-group class="fieldset-group--smushed fieldgroup--paginate">
|
||||
<fieldset-section>
|
||||
<ReactPaginate
|
||||
pageCount={pageCount}
|
||||
pageRangeDisplayed={2}
|
||||
previousLabel="‹"
|
||||
nextLabel="›"
|
||||
activeClassName="pagination__item--selected"
|
||||
pageClassName="pagination__item"
|
||||
previousClassName="pagination__item pagination__item--previous"
|
||||
nextClassName="pagination__item pagination__item--next"
|
||||
breakClassName="pagination__item pagination__item--break"
|
||||
marginPagesDisplayed={2}
|
||||
onPageChange={e => this.changePage(e.selected)}
|
||||
forcePage={page}
|
||||
initialPage={page}
|
||||
disableInitialCallback
|
||||
containerClassName="pagination"
|
||||
/>
|
||||
</fieldset-section>
|
||||
<FormField
|
||||
type="text"
|
||||
name="paginate-input"
|
||||
label={__('Go to page:')}
|
||||
className="paginate-channel"
|
||||
onKeyUp={e => this.paginate(e)}
|
||||
/>
|
||||
</fieldset-group>
|
||||
</Form>
|
||||
)}
|
||||
<Paginate totalPages={pageCount} />
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<div className="main--empty">
|
||||
<section className="card card--section">
|
||||
<header className="card__header">
|
||||
<h2 className="card__title">
|
||||
{__(
|
||||
"You don't have anything saved in history yet, go check out some content on LBRY!"
|
||||
)}
|
||||
{__("You don't have anything saved in history yet, go check out some content on LBRY!")}
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
|
@ -184,4 +122,4 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
|
|||
);
|
||||
}
|
||||
}
|
||||
export default withRouter(UserHistoryPage);
|
||||
export default UserHistoryPage;
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
// @flow
|
||||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { Form, FormField } from 'component/common/form';
|
||||
import ReactPaginate from 'react-paginate';
|
||||
import NavigationHistoryItem from 'component/navigationHistoryItem';
|
||||
|
||||
type HistoryItem = {
|
||||
|
@ -12,14 +10,10 @@ type HistoryItem = {
|
|||
|
||||
type Props = {
|
||||
history: Array<HistoryItem>,
|
||||
page: number,
|
||||
pageCount: number,
|
||||
clearHistoryUri: string => void,
|
||||
params: { page: number },
|
||||
};
|
||||
|
||||
export default function UserHistoryRecent(props: Props) {
|
||||
const { history = [], page, pageCount } = props;
|
||||
export default function NavigationHistoryRecent(props: Props) {
|
||||
const { history = [] } = props;
|
||||
|
||||
return history.length ? (
|
||||
<div className="item-list">
|
||||
|
|
|
@ -9,8 +9,6 @@ const LOADER_TIMEOUT = 1000;
|
|||
type Props = {
|
||||
children: React.Node | Array<React.Node>,
|
||||
pageTitle: ?string,
|
||||
noPadding: ?boolean,
|
||||
extraPadding: ?boolean,
|
||||
notContained: ?boolean, // No max-width, but keep the padding
|
||||
loading: ?boolean,
|
||||
className: ?string,
|
||||
|
@ -46,7 +44,6 @@ class Page extends React.PureComponent<Props, State> {
|
|||
this.beginLoadingTimeout();
|
||||
} else if (!loading && this.loaderTimeout) {
|
||||
clearTimeout(this.loaderTimeout);
|
||||
|
||||
if (showLoader) {
|
||||
this.removeLoader();
|
||||
}
|
||||
|
@ -72,23 +69,14 @@ class Page extends React.PureComponent<Props, State> {
|
|||
loaderTimeout: ?TimeoutID;
|
||||
|
||||
render() {
|
||||
const {
|
||||
pageTitle,
|
||||
children,
|
||||
noPadding,
|
||||
extraPadding,
|
||||
notContained,
|
||||
loading,
|
||||
className,
|
||||
} = this.props;
|
||||
const { children, notContained, loading, className } = this.props;
|
||||
const { showLoader } = this.state;
|
||||
|
||||
return (
|
||||
<main
|
||||
className={classnames('main', className, {
|
||||
'main--contained': !notContained && !noPadding && !extraPadding,
|
||||
'main--no-padding': noPadding,
|
||||
'main--extra-padding': extraPadding,
|
||||
'main--contained': !notContained,
|
||||
'main--not-contained': notContained,
|
||||
})}
|
||||
>
|
||||
{!loading && children}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as PAGES from 'constants/pages';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Route, Switch, withRouter } from 'react-router-dom';
|
||||
import { Route, Redirect, Switch, withRouter } from 'react-router-dom';
|
||||
import SettingsPage from 'page/settings';
|
||||
import HelpPage from 'page/help';
|
||||
import ReportPage from 'page/report';
|
||||
|
@ -58,8 +58,11 @@ export default function AppRouter() {
|
|||
<Route path={`/$/${PAGES.HISTORY}/all`} exact component={NavigationHistory} />
|
||||
|
||||
{/* Below need to go at the end to make sure we don't match any of our pages first */}
|
||||
<Route path="/:claimName/:claimId" component={ShowPage} />
|
||||
<Route path="/:claimName" component={ShowPage} />
|
||||
<Route path="/:claimName" exact component={ShowPage} />
|
||||
<Route path="/:claimName/:claimId" exact component={ShowPage} />
|
||||
|
||||
{/* Route not found. Mostly for people typing crazy urls into the url */}
|
||||
<Route render={() => <Redirect to="/" />} />
|
||||
</Switch>
|
||||
</Scroll>
|
||||
);
|
||||
|
|
17
src/ui/component/shareButton/index.js
Normal file
17
src/ui/component/shareButton/index.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import ShareButton from './view';
|
||||
|
||||
const select = (state, props) => ({});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
{
|
||||
doChannelSubscribe,
|
||||
doChannelUnsubscribe,
|
||||
doOpenModal,
|
||||
doToast,
|
||||
}
|
||||
)(ShareButton);
|
23
src/ui/component/shareButton/view.jsx
Normal file
23
src/ui/component/shareButton/view.jsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
// @flow
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
doOpenModal: (id: string, {}) => void,
|
||||
};
|
||||
|
||||
export default function ShareButton(props: Props) {
|
||||
const { uri, doOpenModal } = props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
button="alt"
|
||||
icon={ICONS.SHARE}
|
||||
label={__('Share Channel')}
|
||||
onClick={() => doOpenModal(MODALS.SOCIAL_SHARE, { uri, speechShareable: true, isChannel: true })}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -26,7 +26,7 @@ class SuggestedSubscriptions extends Component<Props> {
|
|||
}
|
||||
|
||||
return suggested ? (
|
||||
<div className="card__content subscriptions__suggested">
|
||||
<div className="card__content subscriptions__suggested main__item--extend-outside">
|
||||
{suggested.map(({ uri, label }) => (
|
||||
<CategoryList key={uri} category={label} categoryLink={uri} />
|
||||
))}
|
||||
|
|
|
@ -11,20 +11,13 @@ import * as MODALS from 'constants/modal_types';
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import {
|
||||
doConditionalAuthNavigate,
|
||||
doDaemonReady,
|
||||
doAutoUpdate,
|
||||
doOpenModal,
|
||||
doHideModal,
|
||||
} from 'redux/actions/app';
|
||||
import { doConditionalAuthNavigate, doDaemonReady, doAutoUpdate, doOpenModal, doHideModal } from 'redux/actions/app';
|
||||
import { Lbry, doToast, isURIValid, setSearchApi } from 'lbry-redux';
|
||||
import { doDownloadLanguages, doUpdateIsNightAsync } from 'redux/actions/settings';
|
||||
import { doAuthenticate, Lbryio, rewards, doBlackListedOutpointsSubscribe } from 'lbryinc';
|
||||
import { store, history } from 'store';
|
||||
import pjson from 'package.json';
|
||||
import app from './app';
|
||||
import analytics from './analytics';
|
||||
import doLogWarningConsoleMessage from './logWarningConsoleMessage';
|
||||
import { ConnectedRouter, push } from 'connected-react-router';
|
||||
import cookie from 'cookie';
|
||||
|
|
|
@ -1,30 +1,20 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doFetchClaimsByChannel } from 'redux/actions/content';
|
||||
import { PAGE_SIZE } from 'constants/claim';
|
||||
import {
|
||||
makeSelectClaimForUri,
|
||||
makeSelectClaimsInChannelForCurrentPageState,
|
||||
makeSelectFetchingChannelClaims,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectTotalPagesForChannel,
|
||||
makeSelectTitleForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
makeSelectCoverForUri,
|
||||
} from 'lbry-redux';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import ChannelPage from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimsInChannel: makeSelectClaimsInChannelForCurrentPageState(props.uri)(state),
|
||||
fetching: makeSelectFetchingChannelClaims(props.uri)(state),
|
||||
totalPages: makeSelectTotalPagesForChannel(props.uri, PAGE_SIZE)(state),
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
cover: makeSelectCoverForUri(props.uri)(state),
|
||||
channelIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchClaims: (uri, page) => dispatch(doFetchClaimsByChannel(uri, page)),
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
null
|
||||
)(ChannelPage);
|
||||
|
|
|
@ -1,143 +1,86 @@
|
|||
// @flow
|
||||
import * as icons from 'constants/icons';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import React, { useEffect } from 'react';
|
||||
import BusyIndicator from 'component/common/busy-indicator';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import ReactPaginate from 'react-paginate';
|
||||
import SubscribeButton from 'component/subscribeButton';
|
||||
import React from 'react';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import Page from 'component/page';
|
||||
import FileList from 'component/fileList';
|
||||
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
|
||||
import Button from 'component/button';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
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 { formatLbryUriForWeb } from 'util/uri';
|
||||
import ChannelContent from 'component/channelContent';
|
||||
import ChannelAbout from 'component/channelAbout';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
|
||||
const PAGE_VIEW_QUERY = `view`;
|
||||
const ABOUT_PAGE = `about`;
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
totalPages: number,
|
||||
fetching: boolean,
|
||||
params: { page: number },
|
||||
claim: ChannelClaim,
|
||||
claimsInChannel: Array<StreamClaim>,
|
||||
channelIsMine: boolean,
|
||||
fetchClaims: (string, number) => void,
|
||||
title: ?string,
|
||||
cover: ?string,
|
||||
thumbnail: ?string,
|
||||
location: { search: string },
|
||||
history: { push: string => void },
|
||||
openModal: (id: string, { uri: string }) => void,
|
||||
location: UrlLocation,
|
||||
match: { params: { attribute: ?string } },
|
||||
};
|
||||
|
||||
function ChannelPage(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
fetching,
|
||||
claimsInChannel,
|
||||
claim,
|
||||
totalPages,
|
||||
channelIsMine,
|
||||
openModal,
|
||||
fetchClaims,
|
||||
location,
|
||||
history,
|
||||
} = props;
|
||||
|
||||
const { name, permanent_url: permanentUrl } = claim;
|
||||
const { uri, title, cover, history, location } = props;
|
||||
const { channelName, claimName, claimId } = parseURI(uri);
|
||||
const { search } = location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const page = Number(urlParams.get('page')) || 1;
|
||||
const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined;
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch new claims if the channel or page number changes
|
||||
fetchClaims(uri, page);
|
||||
}, [uri, page]);
|
||||
|
||||
const changePage = (pageNumber: number) => {
|
||||
if (!page && pageNumber === 1) {
|
||||
return;
|
||||
// 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 tabIndex = currentView === ABOUT_PAGE ? 1 : 0;
|
||||
const onTabChange = newTabIndex => {
|
||||
let url = formatLbryUriForWeb(uri);
|
||||
if (newTabIndex !== 0) {
|
||||
url += `?${PAGE_VIEW_QUERY}=${ABOUT_PAGE}`;
|
||||
}
|
||||
|
||||
history.push(`?page=${pageNumber}`);
|
||||
};
|
||||
|
||||
const paginate = (e: SyntheticKeyboardEvent<*>) => {
|
||||
// Change page if enter was pressed, and the given page is between the first and the last page
|
||||
const pageFromInput = Number(e.currentTarget.value);
|
||||
|
||||
if (
|
||||
pageFromInput &&
|
||||
e.keyCode === 13 &&
|
||||
!Number.isNaN(pageFromInput) &&
|
||||
pageFromInput > 0 &&
|
||||
pageFromInput <= totalPages
|
||||
) {
|
||||
changePage(pageFromInput);
|
||||
}
|
||||
history.push(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page notContained>
|
||||
<header className="channel-info">
|
||||
<h1 className="media__title media__title--large">
|
||||
{name}
|
||||
{fetching && <BusyIndicator />}
|
||||
</h1>
|
||||
<span>{permanentUrl}</span>
|
||||
<Page notContained className="main--no-padding-top">
|
||||
<header className="channel__cover main__item--extend-outside">
|
||||
{cover && <img className="channel__cover--custom" src={cover} />}
|
||||
|
||||
<div className="channel-info__actions__group">
|
||||
<SubscribeButton uri={permanentUrl} channelName={name} />
|
||||
<Button
|
||||
button="alt"
|
||||
icon={icons.SHARE}
|
||||
label={__('Share Channel')}
|
||||
onClick={() =>
|
||||
openModal(MODALS.SOCIAL_SHARE, { uri, speechShareable: true, isChannel: true })
|
||||
}
|
||||
/>
|
||||
<div className="channel__primary-info">
|
||||
<ChannelThumbnail uri={uri} />
|
||||
|
||||
<div>
|
||||
<h1 className="channel__title">{title || channelName}</h1>
|
||||
<h2 className="channel__url">
|
||||
{claimName}
|
||||
{claimId && `#${claimId}`}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="media-group--list">
|
||||
{claimsInChannel && claimsInChannel.length ? (
|
||||
<FileList sortByHeight hideFilter fileInfos={claimsInChannel} />
|
||||
) : (
|
||||
!fetching && <span className="empty">{__('No content found.')}</span>
|
||||
)}
|
||||
</section>
|
||||
<Tabs onChange={onTabChange} index={tabIndex}>
|
||||
<TabList className="main__item--extend-outside tabs__list--channel-page">
|
||||
<Tab>{__('Content')}</Tab>
|
||||
<Tab>{__('About')}</Tab>
|
||||
<div className="card__actions">
|
||||
<ShareButton uri={uri} />
|
||||
<SubscribeButton uri={uri} />
|
||||
</div>
|
||||
</TabList>
|
||||
|
||||
{(!fetching || (claimsInChannel && claimsInChannel.length)) && totalPages > 1 && (
|
||||
<Form>
|
||||
<fieldset-group class="fieldset-group--smushed fieldgroup--paginate">
|
||||
<fieldset-section>
|
||||
<ReactPaginate
|
||||
pageCount={totalPages}
|
||||
pageRangeDisplayed={2}
|
||||
previousLabel="‹"
|
||||
nextLabel="›"
|
||||
activeClassName="pagination__item--selected"
|
||||
pageClassName="pagination__item"
|
||||
previousClassName="pagination__item pagination__item--previous"
|
||||
nextClassName="pagination__item pagination__item--next"
|
||||
breakClassName="pagination__item pagination__item--break"
|
||||
marginPagesDisplayed={2}
|
||||
onPageChange={e => changePage(e.selected + 1)}
|
||||
forcePage={page - 1}
|
||||
initialPage={page - 1}
|
||||
disableInitialCallback
|
||||
containerClassName="pagination"
|
||||
/>
|
||||
</fieldset-section>
|
||||
|
||||
<FormField
|
||||
className="paginate-channel"
|
||||
onKeyUp={e => paginate(e)}
|
||||
label={__('Go to page:')}
|
||||
type="text"
|
||||
name="paginate-file"
|
||||
/>
|
||||
</fieldset-group>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{!channelIsMine && <HiddenNsfwClaims className="card__content help" uri={uri} />}
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<ChannelContent uri={uri} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ChannelAbout uri={uri} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ class DiscoverPage extends React.PureComponent<Props> {
|
|||
const failedToLoad = !fetchingFeaturedUris && !hasContent;
|
||||
|
||||
return (
|
||||
<Page noPadding isLoading={!hasContent && fetchingFeaturedUris}>
|
||||
<Page notContained isLoading={!hasContent && fetchingFeaturedUris} className="main--no-padding">
|
||||
<FirstRun />
|
||||
{hasContent &&
|
||||
Object.keys(featuredUris).map(category => (
|
||||
|
|
|
@ -23,29 +23,17 @@ class FileListPublished extends React.PureComponent<Props> {
|
|||
return (
|
||||
<Page notContained loading={fetching}>
|
||||
{claims && claims.length ? (
|
||||
<FileList
|
||||
checkPending
|
||||
fileInfos={claims}
|
||||
sortByHeight
|
||||
sortBy={sortBy}
|
||||
page={PAGES.PUBLISHED}
|
||||
/>
|
||||
<FileList checkPending fileInfos={claims} sortByHeight sortBy={sortBy} page={PAGES.PUBLISHED} />
|
||||
) : (
|
||||
<div className="main--empty">
|
||||
<section className="card card--section">
|
||||
<header className="card__header">
|
||||
<h2 className="card__title">
|
||||
{__("It looks like you haven't published anything to LBRY yet.")}
|
||||
</h2>
|
||||
<h2 className="card__title">{__("It looks like you haven't published anything to LBRY yet.")}</h2>
|
||||
</header>
|
||||
|
||||
<div className="card__content">
|
||||
<div className="card__actions card__actions--center">
|
||||
<Button
|
||||
button="primary"
|
||||
navigate="/$/publish"
|
||||
label={__('Publish something new')}
|
||||
/>
|
||||
<Button button="primary" navigate="/$/publish" label={__('Publish something new')} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React, { useEffect, Fragment } from 'react';
|
||||
import { isURIValid, normalizeURI, parseURI } from 'lbry-redux';
|
||||
import FileTile from 'component/fileTile';
|
||||
import ChannelTile from 'component/channelTile';
|
||||
import FileListSearch from 'component/fileListSearch';
|
||||
import Page from 'component/page';
|
||||
import ToolTip from 'component/common/tooltip';
|
||||
import Icon from 'component/common/icon';
|
||||
import SearchOptions from 'component/searchOptions';
|
||||
import Button from 'component/button';
|
||||
|
||||
|
@ -24,7 +21,6 @@ export default function SearchPage(props: Props) {
|
|||
|
||||
let uri;
|
||||
let isChannel;
|
||||
let label;
|
||||
if (isValid) {
|
||||
uri = normalizeURI(urlQuery);
|
||||
({ isChannel } = parseURI(uri));
|
||||
|
@ -34,10 +30,10 @@ export default function SearchPage(props: Props) {
|
|||
if (urlQuery) {
|
||||
doSearch(urlQuery);
|
||||
}
|
||||
}, [urlQuery]);
|
||||
}, [doSearch, urlQuery]);
|
||||
|
||||
return (
|
||||
<Page noPadding>
|
||||
<Page>
|
||||
<section className="search">
|
||||
{urlQuery && (
|
||||
<Fragment>
|
||||
|
@ -58,9 +54,7 @@ export default function SearchPage(props: Props) {
|
|||
<SearchOptions />
|
||||
|
||||
<FileListSearch query={urlQuery} />
|
||||
<div className="card__content help">
|
||||
{__('These search results are provided by LBRY, Inc.')}
|
||||
</div>
|
||||
<div className="card__content help">{__('These search results are provided by LBRY, Inc.')}</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import SuggestedSubscriptions from 'component/subscribeSuggested';
|
||||
import Yrbl from 'component/yrbl';
|
||||
|
@ -12,17 +12,11 @@ type Props = {
|
|||
doShowSuggestedSubs: () => void,
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
const {
|
||||
showSuggested,
|
||||
loadingSuggested,
|
||||
numberOfSubscriptions,
|
||||
doShowSuggestedSubs,
|
||||
onFinish,
|
||||
} = props;
|
||||
export default function SubscriptionsFirstRun(props: Props) {
|
||||
const { showSuggested, loadingSuggested, numberOfSubscriptions, doShowSuggestedSubs, onFinish } = props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div>
|
||||
<Yrbl
|
||||
title={numberOfSubscriptions > 0 ? __('Woohoo!') : __('No subscriptions... yet.')}
|
||||
subtitle={
|
||||
|
@ -52,6 +46,6 @@ export default (props: Props) => {
|
|||
}
|
||||
/>
|
||||
{showSuggested && !loadingSuggested && <SuggestedSubscriptions />}
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import SuggestedSubscriptions from 'component/subscribeSuggested';
|
|||
import MarkAsRead from 'component/subscribeMarkAsRead';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
import Yrbl from 'component/yrbl';
|
||||
import { formatLbryUriForWeb } from 'util/uri';
|
||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||
|
||||
type Props = {
|
||||
viewMode: ViewMode,
|
||||
|
@ -33,27 +33,18 @@ export default (props: Props) => {
|
|||
onChangeAutoDownload,
|
||||
unreadSubscriptions,
|
||||
} = props;
|
||||
|
||||
const index = viewMode === VIEW_ALL ? 0 : 1;
|
||||
const onTabChange = index => (index === 0 ? doSetViewMode(VIEW_ALL) : doSetViewMode(VIEW_LATEST_FIRST));
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{hasSubscriptions && (
|
||||
<section className="card card--section">
|
||||
<div className="card__content card--space-between">
|
||||
<div className="card__actions card__actions--no-margin">
|
||||
<Button
|
||||
disabled={viewMode === VIEW_ALL}
|
||||
className={viewMode === VIEW_ALL && 'button--subscription-view-selected'}
|
||||
button="link"
|
||||
label="All Subscriptions"
|
||||
onClick={() => doSetViewMode(VIEW_ALL)}
|
||||
/>
|
||||
<Button
|
||||
button="link"
|
||||
disabled={viewMode === VIEW_LATEST_FIRST}
|
||||
className={viewMode === VIEW_LATEST_FIRST && 'button--subscription-view-selected'}
|
||||
label={__('Latest Only')}
|
||||
onClick={() => doSetViewMode(VIEW_LATEST_FIRST)}
|
||||
/>
|
||||
</div>
|
||||
<Tabs onChange={onTabChange} index={index}>
|
||||
<TabList className="main__item--extend-outside">
|
||||
<Tab>{__('All Subscriptions')}</Tab>
|
||||
<Tab>{__('Latest Only')}</Tab>
|
||||
|
||||
<Tooltip onComponent body={__('Automatically download new subscriptions.')}>
|
||||
<FormField
|
||||
type="setting"
|
||||
|
@ -64,44 +55,29 @@ export default (props: Props) => {
|
|||
labelOnLeft
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</TabList>
|
||||
|
||||
<HiddenNsfwClaims
|
||||
uris={subscriptions.reduce((arr, { name, claim_id: claimId }) => {
|
||||
if (name && claimId) {
|
||||
arr.push(`lbry://${name}#${claimId}`);
|
||||
}
|
||||
return arr;
|
||||
}, [])}
|
||||
/>
|
||||
|
||||
{!hasSubscriptions && (
|
||||
<Fragment>
|
||||
<Yrbl
|
||||
type="sad"
|
||||
title={__('Oh no! What happened to your subscriptions?')}
|
||||
subtitle={__('These channels look pretty cool.')}
|
||||
/>
|
||||
<SuggestedSubscriptions />
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{hasSubscriptions && (
|
||||
<div className="card__content">
|
||||
{viewMode === VIEW_ALL && (
|
||||
<Fragment>
|
||||
<TabPanels
|
||||
header={
|
||||
<HiddenNsfwClaims
|
||||
uris={subscriptions.reduce((arr, { name, claim_id: claimId }) => {
|
||||
if (name && claimId) {
|
||||
arr.push(`lbry://${name}#${claimId}`);
|
||||
}
|
||||
return arr;
|
||||
}, [])}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TabPanel>
|
||||
<div className="card__title card__title--flex">
|
||||
<span>{__('Your subscriptions')}</span>
|
||||
{unreadSubscriptions.length > 0 && <MarkAsRead />}
|
||||
</div>
|
||||
<FileList hideFilter sortByHeight fileInfos={subscriptions} />
|
||||
</Fragment>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
{viewMode === VIEW_LATEST_FIRST && (
|
||||
<Fragment>
|
||||
<TabPanel>
|
||||
{unreadSubscriptions.length ? (
|
||||
unreadSubscriptions.map(({ channel, uris }) => {
|
||||
const { claimName } = parseURI(channel);
|
||||
|
@ -124,16 +100,24 @@ export default (props: Props) => {
|
|||
})
|
||||
) : (
|
||||
<Fragment>
|
||||
<Yrbl
|
||||
title={__('All caught up!')}
|
||||
subtitle={__('You might like the channels below.')}
|
||||
/>
|
||||
<Yrbl title={__('All caught up!')} subtitle={__('You might like the channels below.')} />
|
||||
<SuggestedSubscriptions />
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
{!hasSubscriptions && (
|
||||
<Fragment>
|
||||
<Yrbl
|
||||
type="sad"
|
||||
title={__('Oh no! What happened to your subscriptions?')}
|
||||
subtitle={__('These channels look pretty cool.')}
|
||||
/>
|
||||
<SuggestedSubscriptions />
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -26,7 +26,7 @@ type Props = {
|
|||
showSuggestedSubs: boolean,
|
||||
};
|
||||
|
||||
export default class extends PureComponent<Props> {
|
||||
export default class SubscriptionsPage extends PureComponent<Props> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
|
@ -78,7 +78,7 @@ export default class extends PureComponent<Props> {
|
|||
// Only pass in the loading prop if there are no subscriptions
|
||||
// If there are any, let the page update in the background
|
||||
// The loading prop removes children and shows a loading spinner
|
||||
<Page notContained loading={loading && !subscribedChannels}>
|
||||
<Page notContained loading={loading && !subscribedChannels} className="main--no-padding-top">
|
||||
{firstRunCompleted ? (
|
||||
<UserSubscriptions
|
||||
viewMode={viewMode}
|
||||
|
|
|
@ -10,10 +10,7 @@ function getLocalStorageSetting(setting, fallback) {
|
|||
const reducers = {};
|
||||
const defaultState = {
|
||||
clientSettings: {
|
||||
[SETTINGS.INSTANT_PURCHASE_ENABLED]: getLocalStorageSetting(
|
||||
SETTINGS.INSTANT_PURCHASE_ENABLED,
|
||||
false
|
||||
),
|
||||
[SETTINGS.INSTANT_PURCHASE_ENABLED]: getLocalStorageSetting(SETTINGS.INSTANT_PURCHASE_ENABLED, false),
|
||||
[SETTINGS.INSTANT_PURCHASE_MAX]: getLocalStorageSetting(SETTINGS.INSTANT_PURCHASE_MAX, {
|
||||
currency: 'LBC',
|
||||
amount: 0.1,
|
||||
|
@ -21,26 +18,18 @@ const defaultState = {
|
|||
[SETTINGS.SHOW_NSFW]: getLocalStorageSetting(SETTINGS.SHOW_NSFW, false),
|
||||
[SETTINGS.SHOW_UNAVAILABLE]: getLocalStorageSetting(SETTINGS.SHOW_UNAVAILABLE, true),
|
||||
[SETTINGS.NEW_USER_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.NEW_USER_ACKNOWLEDGED, false),
|
||||
[SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED]: getLocalStorageSetting(
|
||||
SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED,
|
||||
false
|
||||
),
|
||||
[SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, false),
|
||||
[SETTINGS.INVITE_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.INVITE_ACKNOWLEDGED, false),
|
||||
[SETTINGS.FIRST_RUN_COMPLETED]: getLocalStorageSetting(SETTINGS.FIRST_RUN_COMPLETED, false),
|
||||
[SETTINGS.CREDIT_REQUIRED_ACKNOWLEDGED]: false, // this needs to be re-acknowledged every run
|
||||
[SETTINGS.LANGUAGE]: getLocalStorageSetting(SETTINGS.LANGUAGE, 'en'),
|
||||
[SETTINGS.THEME]: getLocalStorageSetting(SETTINGS.THEME, 'dark'),
|
||||
[SETTINGS.THEMES]: getLocalStorageSetting(SETTINGS.THEMES, []),
|
||||
[SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: getLocalStorageSetting(
|
||||
SETTINGS.AUTOMATIC_DARK_MODE_ENABLED,
|
||||
false
|
||||
),
|
||||
[SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: getLocalStorageSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false),
|
||||
[SETTINGS.AUTOPLAY]: getLocalStorageSetting(SETTINGS.AUTOPLAY, false),
|
||||
[SETTINGS.RESULT_COUNT]: Number(getLocalStorageSetting(SETTINGS.RESULT_COUNT, 50)),
|
||||
[SETTINGS.AUTO_DOWNLOAD]: getLocalStorageSetting(SETTINGS.AUTO_DOWNLOAD, true),
|
||||
[SETTINGS.OS_NOTIFICATIONS_ENABLED]: Boolean(
|
||||
getLocalStorageSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED, true)
|
||||
),
|
||||
[SETTINGS.OS_NOTIFICATIONS_ENABLED]: Boolean(getLocalStorageSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED, true)),
|
||||
},
|
||||
isNight: false,
|
||||
languages: {},
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
@import 'component/subscriptions';
|
||||
@import 'component/syntax-highlighter';
|
||||
@import 'component/table';
|
||||
@import 'component/tabs';
|
||||
@import 'component/time';
|
||||
@import 'component/toggle';
|
||||
@import 'component/tooltip';
|
||||
|
|
|
@ -3,10 +3,13 @@
|
|||
border: 1px solid $lbry-gray-1;
|
||||
margin-bottom: var(--spacing-vertical-xlarge);
|
||||
position: relative;
|
||||
border-radius: var(--card-radius);
|
||||
box-shadow: var(--box-shadow) $lbry-gray-1;
|
||||
|
||||
html[data-mode='dark'] & {
|
||||
background-color: rgba($lbry-white, 0.1);
|
||||
border-color: rgba($lbry-white, 0.1);
|
||||
box-shadow: var(--box-shadow) darken($lbry-gray-1, 80%);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,7 +49,7 @@
|
|||
font-size: 1.15rem;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: var(--spacing-vertical-large);
|
||||
margin-right: var(--spacing-vertical-medium);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,6 +121,37 @@
|
|||
.card__list {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-vertical-medium);
|
||||
|
||||
// Depending on screen width, the amount of items in
|
||||
// each row change and are auto-sized
|
||||
|
||||
@media (min-width: 2001px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 10), 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 1801px) and (max-width: 2000px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 8), 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 1551px) and (max-width: 1800px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 7), 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 1051px) and (max-width: 1550px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 6), 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 901px) and (max-width: 1050px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 5), 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 751px) and (max-width: 900px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 4), 1fr));
|
||||
}
|
||||
|
||||
@media (max-width: 750px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 3), 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.card__list--rewards {
|
||||
|
@ -137,8 +171,8 @@
|
|||
|
||||
.card__message {
|
||||
border-left: 0.5rem solid;
|
||||
padding: var(--spacing-vertical-medium) var(--spacing-vertical-medium)
|
||||
var(--spacing-vertical-medium) var(--spacing-vertical-large);
|
||||
padding: var(--spacing-vertical-medium) var(--spacing-vertical-medium) var(--spacing-vertical-medium)
|
||||
var(--spacing-vertical-large);
|
||||
|
||||
&:not(&--error):not(&--failure):not(&--success) {
|
||||
background-color: rgba($lbry-teal-1, 0.1);
|
||||
|
@ -184,6 +218,7 @@
|
|||
.card__title {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
padding-bottom: var(--spacing-vertical-medium);
|
||||
|
||||
+ .card__content {
|
||||
margin-top: var(--spacing-vertical-medium);
|
||||
|
|
|
@ -1,27 +1,71 @@
|
|||
.channel-info {
|
||||
.media__title {
|
||||
display: block;
|
||||
user-select: text;
|
||||
margin-bottom: var(--spacing-vertical-medium);
|
||||
}
|
||||
$cover-z-index: 0;
|
||||
$metadata-z-index: 1;
|
||||
|
||||
.channel-info__actions__group {
|
||||
margin-bottom: var(--spacing-vertical-large);
|
||||
}
|
||||
}
|
||||
|
||||
.channel-info__actions {
|
||||
.channel__cover {
|
||||
background-image: linear-gradient(to right, $lbry-indigo-4, $lbry-cyan-5 80%);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
box-sizing: content-box;
|
||||
color: $lbry-white;
|
||||
}
|
||||
|
||||
.channel-info__actions__group {
|
||||
@extend .media__action-group;
|
||||
@extend .media__action-group--large;
|
||||
.channel__cover--custom {
|
||||
z-index: $cover-z-index;
|
||||
align-self: flex-start;
|
||||
position: absolute;
|
||||
object-fit: cover;
|
||||
filter: brightness(40%);
|
||||
}
|
||||
|
||||
.channel-name {
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
.channel__cover,
|
||||
.channel__cover--custom {
|
||||
height: var(--cover-photo-height);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.channel__thumbnail {
|
||||
position: absolute;
|
||||
left: var(--spacing-main-padding);
|
||||
height: var(--channel-thumbnail-size);
|
||||
width: var(--channel-thumbnail-size);
|
||||
background-color: $lbry-gray-3;
|
||||
background-image: linear-gradient(to right, $lbry-white, $lbry-gray-3 80%);
|
||||
background-size: cover;
|
||||
box-shadow: 0px 8px 40px -3px $lbry-black;
|
||||
}
|
||||
|
||||
.channel__thumbnail--custom {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.channel__thumbnail,
|
||||
.channel__thumbnail--custom {
|
||||
border-radius: var(--card-radius);
|
||||
}
|
||||
|
||||
.channel__primary-info {
|
||||
// Ensure the profile pic/title sit ontop of the default cover background
|
||||
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
|
||||
margin-left: calc(var(--channel-thumbnail-size) + var(--spacing-main-padding));
|
||||
padding-left: var(--spacing-vertical-large);
|
||||
padding-bottom: var(--spacing-vertical-medium);
|
||||
}
|
||||
|
||||
.channel__title {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.channel__url {
|
||||
font-size: 1.2rem;
|
||||
user-select: all;
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
|
||||
// .channel__description {
|
||||
// font-size: 1.3rem;
|
||||
// margin: var(--spacing-vertical-large) 0;
|
||||
// }
|
||||
|
|
|
@ -87,7 +87,7 @@ fieldset-group {
|
|||
}
|
||||
|
||||
&.fieldgroup--paginate {
|
||||
margin-top: var(--spacing-vertical-medium);
|
||||
margin: var(--spacing-vertical-large) 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
.main-wrapper {
|
||||
position: absolute;
|
||||
top: var(--header-height);
|
||||
left: var(--side-nav-width);
|
||||
min-height: calc(100% - var(--header-height));
|
||||
width: calc(100% - var(--side-nav-width));
|
||||
width: 100%;
|
||||
background-color: mix($lbry-white, $lbry-gray-1, 70%);
|
||||
|
||||
html[data-mode='dark'] & {
|
||||
background-color: $lbry-black;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
left: var(--side-nav-width);
|
||||
width: calc(100% - var(--side-nav-width));
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
|
@ -19,13 +23,23 @@
|
|||
|
||||
.main--contained {
|
||||
max-width: 900px;
|
||||
padding: var(--spacing-main-padding);
|
||||
}
|
||||
|
||||
.main:not(.main--no-padding) {
|
||||
padding: var(--spacing-vertical-large);
|
||||
.main--not-contained {
|
||||
padding: var(--spacing-main-padding);
|
||||
}
|
||||
|
||||
.main--no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main--no-padding-top {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.main--file-page {
|
||||
padding: var(--spacing-main-padding);
|
||||
max-width: var(--file-page-max-width);
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-vertical-large);
|
||||
|
@ -35,7 +49,7 @@
|
|||
'content content'
|
||||
'info related';
|
||||
|
||||
@media (min-width: 1470px) {
|
||||
@media (min-width: 1300px) {
|
||||
grid-template-areas:
|
||||
'content related'
|
||||
'info related';
|
||||
|
@ -60,3 +74,16 @@
|
|||
margin-bottom: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// On pages that are not contained, they still might want to have items inside the page
|
||||
// that extend the full width ex: cover photo
|
||||
// But the components inside of those pages should be still have "page" padding
|
||||
.main__item--extend-outside {
|
||||
$main-width: calc(100vw - var(--side-nav-width));
|
||||
width: $main-width;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
right: 50%;
|
||||
margin-left: calc(-50vw + (var(--side-nav-width) * 0.5));
|
||||
margin-right: calc(-50vw + (var(--side-nav-width) * 0.5));
|
||||
}
|
||||
|
|
|
@ -345,42 +345,6 @@
|
|||
// G R O U P
|
||||
|
||||
.media-group--list {
|
||||
.card__list {
|
||||
padding-top: var(--spacing-vertical-large);
|
||||
padding-bottom: var(--spacing-vertical-large);
|
||||
|
||||
// Depending on screen width, the amount of items in
|
||||
// each row change and are auto-sized
|
||||
|
||||
@media (min-width: 2001px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 10), 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 1801px) and (max-width: 2000px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 8), 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 1551px) and (max-width: 1800px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 7), 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 1051px) and (max-width: 1550px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 6), 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 901px) and (max-width: 1050px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 5), 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 751px) and (max-width: 900px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 4), 1fr));
|
||||
}
|
||||
|
||||
@media (max-width: 750px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(calc(100% / 3), 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.media-card {
|
||||
display: inline-block;
|
||||
margin-bottom: var(--spacing-vertical-large);
|
||||
|
@ -473,11 +437,7 @@
|
|||
background-image: linear-gradient(to right, $lbry-white 80%, transparent 100%);
|
||||
|
||||
html[data-mode='dark'] & {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
mix($lbry-white, $lbry-gray-3, 50%) 80%,
|
||||
transparent 100%
|
||||
);
|
||||
background-image: linear-gradient(to right, mix($lbry-white, $lbry-gray-3, 50%) 80%, transparent 100%);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,11 +35,7 @@
|
|||
right: 0;
|
||||
|
||||
// TODO: Make this gradient "to bottom" on mobile view
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
rgba(mix($lbry-blue-3, $lbry-gray-4, 15%), 0.2) 100%
|
||||
);
|
||||
background-image: linear-gradient(to right, transparent, rgba(mix($lbry-blue-3, $lbry-gray-4, 15%), 0.2) 100%);
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
|
@ -93,7 +89,7 @@
|
|||
}
|
||||
|
||||
&::before {
|
||||
width: 0.5rem;
|
||||
width: var(--tab-indicator-size);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,13 @@
|
|||
.search__header {
|
||||
background-color: $lbry-black;
|
||||
color: $lbry-white;
|
||||
padding: var(--spacing-vertical-large);
|
||||
|
||||
.placeholder {
|
||||
background-color: rgba($lbry-white, 0.1);
|
||||
}
|
||||
|
||||
.media__subtext {
|
||||
color: rgba($lbry-white, 0.6);
|
||||
}
|
||||
|
||||
.media__subtitle {
|
||||
color: rgba($lbry-white, 0.9);
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
html[data-mode='dark'] & {
|
||||
background-color: transparent;
|
||||
border-bottom: 1px solid rgba($lbry-white, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.search__title {
|
||||
|
@ -37,7 +25,7 @@
|
|||
}
|
||||
|
||||
.search__results-wrapper {
|
||||
padding: var(--spacing-vertical-large);
|
||||
margin: var(--spacing-vertical-large);
|
||||
}
|
||||
|
||||
.search__results-section {
|
||||
|
@ -53,7 +41,7 @@
|
|||
|
||||
.search__options-wrapper {
|
||||
font-size: 1.25em;
|
||||
padding-bottom: var(--spacing-vertical-large);
|
||||
margin-bottom: var(--spacing-vertical-large);
|
||||
}
|
||||
|
||||
.search__options {
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
// The gerbil is tied to subscriptions currently, but this style should move to it's own file once
|
||||
// the gerbil is added in more places with different layouts
|
||||
.subscriptions__suggested {
|
||||
animation: expand 0.2s;
|
||||
left: -2rem;
|
||||
position: relative;
|
||||
width: calc(100% + 4rem);
|
||||
}
|
||||
|
|
62
src/ui/scss/component/tabs.scss
Normal file
62
src/ui/scss/component/tabs.scss
Normal file
|
@ -0,0 +1,62 @@
|
|||
.tabs {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tabs__list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: $lbry-black;
|
||||
color: $lbry-white;
|
||||
padding: var(--spacing-vertical-medium) var(--spacing-main-padding);
|
||||
|
||||
& > *:not(.tab) {
|
||||
// If there is anything after the tabs, render it on the opposite side of the page
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
[data-mode='dark'] & {
|
||||
background-color: darken($lbry-black, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.tabs__list--channel-page {
|
||||
padding-left: calc(var(--channel-thumbnail-size) + var(--spacing-main-padding) + var(--spacing-vertical-large));
|
||||
}
|
||||
|
||||
.tab {
|
||||
margin-right: var(--spacing-vertical-large);
|
||||
padding: 5px 0;
|
||||
font-weight: 700;
|
||||
font-size: var(--tab-header-size);
|
||||
color: $lbry-white;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
bottom: calc(var(--tab-indicator-size) * -2);
|
||||
height: 0;
|
||||
width: 100%;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.tab__divider {
|
||||
position: absolute;
|
||||
margin-top: calc(var(--tab-indicator-size) * -1);
|
||||
}
|
||||
|
||||
.tab::after,
|
||||
.tab__divider {
|
||||
display: block;
|
||||
transition: all var(--animation-duration) var(--animation-style);
|
||||
}
|
||||
|
||||
.tab:hover::after,
|
||||
.tab__divider {
|
||||
height: var(--tab-indicator-size);
|
||||
background-color: $lbry-teal-3;
|
||||
}
|
||||
|
||||
.tab__panel {
|
||||
margin-top: var(--spacing-vertical-large);
|
||||
}
|
|
@ -134,6 +134,7 @@ code {
|
|||
padding: 1rem;
|
||||
margin-top: var(--spacing-vertical-large);
|
||||
margin-bottom: var(--spacing-vertical-large);
|
||||
border-radius: 5px;
|
||||
|
||||
html[data-mode='dark'] & {
|
||||
color: inherit;
|
||||
|
|
|
@ -79,16 +79,6 @@ dl {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
dt {
|
||||
float: left;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
dd {
|
||||
float: left;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
border: 1px solid $lbry-gray-2;
|
||||
padding: $spacing-vertical * 1/3;
|
||||
|
|
|
@ -16,6 +16,7 @@ $large-breakpoint: 1921px;
|
|||
--spacing-vertical-medium: calc(2rem / 2);
|
||||
--spacing-vertical-large: 2rem;
|
||||
--spacing-vertical-xlarge: 3rem;
|
||||
--spacing-main-padding: var(--spacing-vertical-xlarge);
|
||||
|
||||
--file-page-max-width: 1787px;
|
||||
--file-max-height: 788px;
|
||||
|
@ -23,10 +24,16 @@ $large-breakpoint: 1921px;
|
|||
|
||||
--video-aspect-ratio: 56.25%; // 9 x 16
|
||||
|
||||
--channel-thumbnail-size: 10rem;
|
||||
|
||||
// Text
|
||||
--text-max-width: 660px;
|
||||
--text-link-padding: 4px;
|
||||
|
||||
// Tabs
|
||||
--tab-indicator-size: 0.5rem;
|
||||
--tab-header-size: 1.5rem; // Needs to be static so the animated styling always works
|
||||
|
||||
// Input
|
||||
--input-border-size: 1px;
|
||||
|
||||
|
@ -44,8 +51,9 @@ $large-breakpoint: 1921px;
|
|||
--search-modal-input-height: 70px;
|
||||
|
||||
// Card
|
||||
--card-radius: 2px;
|
||||
--card-radius: 5px;
|
||||
--card-max-width: 1000px;
|
||||
--card-box-shadow: 0px 8px 20px;
|
||||
|
||||
// File
|
||||
--file-tile-media-height: 125px;
|
||||
|
@ -62,10 +70,11 @@ $large-breakpoint: 1921px;
|
|||
--modal-width: 440px;
|
||||
|
||||
// Animation :)
|
||||
--animation-duration: 0.3s;
|
||||
--animation-style: cubic-bezier(0.55, 0, 0.1, 1);
|
||||
--animation-duration: 0.2s;
|
||||
--animation-style: ease-in-out;
|
||||
|
||||
// Image
|
||||
--thumbnail-preview-height: 100px;
|
||||
--thumbnail-preview-width: 177px;
|
||||
--cover-photo-height: 250px;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ const webConfig = {
|
|||
path: __dirname + '/dist/web',
|
||||
publicPath: '/',
|
||||
},
|
||||
devServer: {
|
||||
historyApiFallback: true,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
|
|
53
yarn.lock
53
yarn.lock
|
@ -885,6 +885,43 @@
|
|||
resolved "https://registry.yarnpkg.com/@posthtml/esm/-/esm-1.0.0.tgz#09bcb28a02438dcee22ad1970ca1d85a000ae0cf"
|
||||
integrity sha512-dEVG+ITnvqKGa4v040tP+n8LOKOqr94qjLva7bE5pnfm2KHJwsKz69J4KMxgWLznbpBJzy8vQfCayEk3vLZnZQ==
|
||||
|
||||
"@reach/auto-id@^0.2.0":
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.2.0.tgz#97f9e48fe736aa5c6f4f32cf73c1f19d005f8550"
|
||||
integrity sha512-lVK/svL2HuQdp7jgvlrLkFsUx50Az9chAhxpiPwBqcS83I2pVWvXp98FOcSCCJCV++l115QmzHhFd+ycw1zLBg==
|
||||
|
||||
"@reach/component-component@^0.1.3":
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@reach/component-component/-/component-component-0.1.3.tgz#5d156319572dc38995b246f81878bc2577c517e5"
|
||||
integrity sha512-a1USH7L3bEfDdPN4iNZGvMEFuBfkdG+QNybeyDv8RloVFgZYRoM+KGXyy2KOfEnTUM8QWDRSROwaL3+ts5Angg==
|
||||
|
||||
"@reach/observe-rect@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.0.3.tgz#2ea3dcc369ab22bd9f050a92ea319321356a61e8"
|
||||
integrity sha1-LqPcw2mrIr2fBQqS6jGTITVqYeg=
|
||||
|
||||
"@reach/rect@^0.2.1":
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@reach/rect/-/rect-0.2.1.tgz#7343020174c90e2290b844d17c03fd9c78e6b601"
|
||||
integrity sha512-aZ9RsNHDMQ3zETonikqu9/85iXxj+LPqZ9Gr9UAncj3AufYmGeWG3XG6b37B+7ORH+mkhVpLU2ZlIWxmOe9Cqg==
|
||||
dependencies:
|
||||
"@reach/component-component" "^0.1.3"
|
||||
"@reach/observe-rect" "^1.0.3"
|
||||
|
||||
"@reach/tabs@^0.1.5":
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@reach/tabs/-/tabs-0.1.5.tgz#0a3a8c863cc50ac661b3a66afea0f9315c8d8b2b"
|
||||
integrity sha512-thQKlbN7kN/YoFfBjTVxAlRlYor0dFg7QnZwUN9v1OYFLHMoPpmwaQkae8mAEibRb4BPGgjnoSpdfco2lzP37A==
|
||||
dependencies:
|
||||
"@reach/auto-id" "^0.2.0"
|
||||
"@reach/utils" "^0.2.2"
|
||||
warning "^4.0.2"
|
||||
|
||||
"@reach/utils@^0.2.2":
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.2.2.tgz#c3a05ae9fd1f921988ae8a89b5a0d28d1a2b92df"
|
||||
integrity sha512-jYeIi46AA5jh2gfdXD/nInUYfeLp3girRafiajP7AVHF6B4hpYAzUSx/ZH4xmPyf5alut5rml2DHxrv+X+Xu+A==
|
||||
|
||||
"@samverschueren/stream-to-observable@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"
|
||||
|
@ -4092,6 +4129,11 @@ eslint-plugin-promise@^4.0.1:
|
|||
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz#1e08cb68b5b2cd8839f8d5864c796f56d82746db"
|
||||
integrity sha512-faAHw7uzlNPy7b45J1guyjazw28M+7gJokKUjC5JSFoYfUEyy6Gw/i7YQvmv2Yk00sUjWcmzXQLpU1Ki/C2IZQ==
|
||||
|
||||
eslint-plugin-react-hooks@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.6.0.tgz#348efcda8fb426399ac7b8609607c7b4025a6f5f"
|
||||
integrity sha512-lHBVRIaz5ibnIgNG07JNiAuBUeKhEf8l4etNx5vfAEwqQ5tcuK3jV9yjmopPgQDagQb7HwIuQVsE3IVcGrRnag==
|
||||
|
||||
eslint-plugin-react@^7.7.0:
|
||||
version "7.12.4"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.12.4.tgz#b1ecf26479d61aee650da612e425c53a99f48c8c"
|
||||
|
@ -6456,9 +6498,9 @@ lazy-val@^1.0.3, lazy-val@^1.0.4:
|
|||
yargs "^13.2.2"
|
||||
zstd-codec "^0.1.1"
|
||||
|
||||
lbry-redux@lbryio/lbry-redux#cc42856676541120b088e4228c04246ba8ff3274:
|
||||
lbry-redux@lbryio/lbry-redux#459bea2257d61003e591daf169fefe9624522680:
|
||||
version "0.0.1"
|
||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/cc42856676541120b088e4228c04246ba8ff3274"
|
||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/459bea2257d61003e591daf169fefe9624522680"
|
||||
dependencies:
|
||||
proxy-polyfill "0.1.6"
|
||||
reselect "^3.0.0"
|
||||
|
@ -11671,6 +11713,13 @@ warning@^3.0.0:
|
|||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
warning@^4.0.2:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
watchpack@^1.5.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
|
||||
|
|
Loading…
Reference in a new issue