Merge pull request #2462 from lbryio/dev

redesign channel page
This commit is contained in:
Sean Yesmunt 2019-05-07 15:07:14 -04:00 committed by GitHub
commit c8641a0315
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 851 additions and 483 deletions

View file

@ -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",

View file

@ -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"]
}
}

View file

@ -1,5 +1,5 @@
{
"trailingComma": "es5",
"printWidth": 100,
"printWidth": 120,
"singleQuote": true
}
}

View file

@ -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"

View file

@ -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,
},
};
};

View 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);

View 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;

View 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);

View 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);

View 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);

View 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;

View 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);

View 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 };

View file

@ -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>
)

View file

@ -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;

View file

@ -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">

View file

@ -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}

View file

@ -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>
);

View 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);

View 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 })}
/>
);
}

View file

@ -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} />
))}

View file

@ -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';

View file

@ -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);

View file

@ -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>
);
}

View file

@ -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 => (

View file

@ -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>

View file

@ -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>
)}

View file

@ -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>
);
};
}

View file

@ -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>
);

View file

@ -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}

View file

@ -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: {},

View file

@ -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';

View file

@ -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);

View file

@ -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;
// }

View file

@ -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;

View file

@ -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));
}

View file

@ -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%);
}
}

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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);
}

View 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);
}

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -18,6 +18,9 @@ const webConfig = {
path: __dirname + '/dist/web',
publicPath: '/',
},
devServer: {
historyApiFallback: true,
},
module: {
rules: [
{

View file

@ -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"