Redesign groundwork (home/search)

This commit is contained in:
Sean Yesmunt 2018-01-04 00:05:20 -05:00
parent d2011551a9
commit 7d492ae1fc
61 changed files with 2151 additions and 1445 deletions

View file

@ -18,5 +18,6 @@ module.name_mapper='^types\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/types\1'
module.name_mapper='^component\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/component\1' module.name_mapper='^component\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/component\1'
module.name_mapper='^page\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/page\1' module.name_mapper='^page\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/page\1'
module.name_mapper='^lbry\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/lbry\1' module.name_mapper='^lbry\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/lbry\1'
module.name_mapper='^modal\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/modal\1'
[strict] [strict]

3
flow-typed/react-modal.js vendored Normal file
View file

@ -0,0 +1,3 @@
declare module 'react-modal' {
declare module.exports: any;
}

25
npm-debug.log.2899857694 Normal file
View file

@ -0,0 +1,25 @@
0 info it worked if it ends with ok
1 verbose cli [ '/Users/seanyesmunt/.nvm/versions/node/v6.12.0/bin/node',
1 verbose cli '/Users/seanyesmunt/.nvm/versions/node/v6.12.0/bin/npm',
1 verbose cli 'config',
1 verbose cli '--loglevel=warn',
1 verbose cli 'get',
1 verbose cli 'prefix' ]
2 info using npm@3.10.10
3 info using node@v6.12.0
4 verbose exit [ 0, true ]
5 verbose stack Error: write EPIPE
5 verbose stack at exports._errnoException (util.js:1020:11)
5 verbose stack at WriteWrap.afterWrite (net.js:800:14)
6 verbose cwd /Users/seanyesmunt/Workspace/lbry/lbry-app
7 error Darwin 17.2.0
8 error argv "/Users/seanyesmunt/.nvm/versions/node/v6.12.0/bin/node" "/Users/seanyesmunt/.nvm/versions/node/v6.12.0/bin/npm" "config" "--loglevel=warn" "get" "prefix"
9 error node v6.12.0
10 error npm v3.10.10
11 error code EPIPE
12 error errno EPIPE
13 error syscall write
14 error write EPIPE
15 error If you need help, you may report this error at:
15 error <https://github.com/npm/npm/issues>
16 verbose exit [ 1, true ]

View file

@ -32,6 +32,7 @@
"amplitude-js": "^4.0.0", "amplitude-js": "^4.0.0",
"bluebird": "^3.5.1", "bluebird": "^3.5.1",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"dom-scroll-into-view": "^1.2.1",
"electron-dl": "^1.6.0", "electron-dl": "^1.6.0",
"formik": "^0.10.4", "formik": "^0.10.4",
"from2": "^2.3.0", "from2": "^2.3.0",

View file

@ -1,3 +1,4 @@
// @flow
import React from 'react'; import React from 'react';
import Router from 'component/router/index'; import Router from 'component/router/index';
import Header from 'component/header'; import Header from 'component/header';
@ -6,61 +7,83 @@ import ModalRouter from 'modal/modalRouter';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
import throttle from 'util/throttle'; import throttle from 'util/throttle';
class App extends React.PureComponent { type Props = {
alertError: (string | {}) => void,
recordScroll: number => void,
currentStackIndex: number,
currentPageAttributes: { path: string, scrollY: number },
pageTitle: ?string,
};
class App extends React.PureComponent<Props> {
constructor() { constructor() {
super(); super();
this.mainContent = undefined; this.mainContent = undefined;
(this: any).scrollListener = this.scrollListener.bind(this);
} }
componentWillMount() { componentWillMount() {
const { alertError } = this.props; const { alertError } = this.props;
document.addEventListener('unhandledError', event => { // TODO: create type for this object
// it lives in jsonrpc.js
document.addEventListener('unhandledError', (event: any) => {
alertError(event.detail); alertError(event.detail);
}); });
} }
componentDidMount() { componentDidMount() {
const { recordScroll } = this.props;
const mainContent = document.getElementById('main-content'); const mainContent = document.getElementById('main-content');
this.mainContent = mainContent; this.mainContent = mainContent;
const scrollListener = () => recordScroll(this.mainContent.scrollTop); if (this.mainContent) {
this.mainContent.addEventListener('scroll', throttle(this.scrollListener, 750));
this.mainContent.addEventListener('scroll', throttle(scrollListener, 750)); }
ReactModal.setAppElement('#window'); // fuck this ReactModal.setAppElement('#window'); // fuck this
} }
componentWillUnmount() { componentWillReceiveProps(props: Props) {
this.mainContent.removeEventListener('scroll', this.scrollListener); const { pageTitle } = props;
this.setTitleFromProps(pageTitle);
} }
componentWillReceiveProps(props) { componentDidUpdate(prevProps: Props) {
this.setTitleFromProps(props);
}
componentDidUpdate(prevProps) {
const { currentStackIndex: prevStackIndex } = prevProps; const { currentStackIndex: prevStackIndex } = prevProps;
const { currentStackIndex, currentPageAttributes } = this.props; const { currentStackIndex, currentPageAttributes } = this.props;
if (currentStackIndex !== prevStackIndex) { if (this.mainContent && currentStackIndex !== prevStackIndex) {
this.mainContent.scrollTop = currentPageAttributes.scrollY || 0; this.mainContent.scrollTop = currentPageAttributes.scrollY || 0;
} }
} }
setTitleFromProps(props) { componentWillUnmount() {
window.document.title = props.pageTitle || 'LBRY'; if (this.mainContent) {
// having issues with this
// $FlowFixMe
this.mainContent.removeEventListener('scroll');
} }
}
setTitleFromProps = (title: ?string) => {
window.document.title = title || 'LBRY';
};
scrollListener() {
const { recordScroll } = this.props;
if (this.mainContent) {
recordScroll(this.mainContent.scrollTop);
}
}
mainContent: ?HTMLElement;
render() { render() {
return ( return (
<div id="window"> <div id="window">
<Theme /> <Theme />
<Header /> <Header />
<div id="main-content">
<Router /> <Router />
</div>
<ModalRouter /> <ModalRouter />
</div> </div>
); );

View file

@ -1,48 +1,18 @@
// @flow
import React from 'react'; import React from 'react';
class CardMedia extends React.PureComponent { type Props = {
static AUTO_THUMB_CLASSES = [ thumbnail: ?string, // externally sourced image
'purple', };
'red',
'pink',
'indigo',
'blue',
'light-blue',
'cyan',
'teal',
'green',
'yellow',
'orange',
];
componentWillMount() {
this.setState({
autoThumbClass:
CardMedia.AUTO_THUMB_CLASSES[
Math.floor(Math.random() * CardMedia.AUTO_THUMB_CLASSES.length)
],
});
}
class CardMedia extends React.PureComponent<Props> {
render() { render() {
const { title, thumbnail } = this.props; const { thumbnail } = this.props;
const atClass = this.state.autoThumbClass;
if (thumbnail) { if (thumbnail) {
return <div className="card__media" style={{ backgroundImage: `url('${thumbnail}')` }} />; return <div className="card__media" style={{ backgroundImage: `url('${thumbnail}')` }} />;
} }
return ( return <div className="card__media card__media--autothumb">LBRY</div>;
<div className={`card__media card__media--autothumb ${atClass}`}>
<div className="card__autothumb__text">
{title &&
title
.replace(/\s+/g, '')
.substring(0, Math.min(title.replace(' ', '').length, 5))
.toUpperCase()}
</div>
</div>
);
} }
} }

View file

@ -1,3 +1,6 @@
// just disabling the linter because this file shouldn't even exist
// will continue to move components over to /components/common/{comp} - sean
/* eslint-disable */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { formatCredits, formatFullPrice } from 'util/formatCredits'; import { formatCredits, formatFullPrice } from 'util/formatCredits';
@ -170,3 +173,4 @@ export class Thumbnail extends React.PureComponent {
); );
} }
} }
/* eslint-enable */

View file

@ -0,0 +1,255 @@
// @flow
import React from 'react';
import lbryuri from 'lbryuri';
import ToolTip from 'component/common/tooltip';
import FileCard from 'component/fileCard';
import Button from 'component/link';
type Props = {
category: string,
names: Array<string>,
categoryLink?: string,
};
type State = {
canScrollNext: boolean,
canScrollPrevious: boolean,
};
class CategoryList extends React.PureComponent<Props, State> {
constructor() {
super();
this.state = {
canScrollPrevious: false,
canScrollNext: false,
};
(this: any).handleScrollNext = this.handleScrollNext.bind(this);
(this: any).handleScrollPrevious = this.handleScrollPrevious.bind(this);
this.rowItems = undefined;
}
componentDidMount() {
const cardRow = this.rowItems;
if (cardRow) {
const cards = cardRow.getElementsByTagName('section');
const lastCard = cards[cards.length - 1];
const isCompletelyVisible = this.isCardVisible(lastCard);
if (!isCompletelyVisible) {
// not sure how we can avoid doing this
/* eslint-disable react/no-did-mount-set-state */
this.setState({
canScrollNext: true,
});
/* eslint-enable react/no-did-mount-set-state */
}
}
}
rowItems: ?HTMLDivElement;
handleScroll(cardRow: HTMLDivElement, scrollTarget: number) {
const cards = cardRow.getElementsByTagName('section');
const animationCallback = () => {
const firstCard = cards[0];
const lastCard = cards[cards.length - 1];
const firstCardVisible = this.isCardVisible(firstCard);
const lastCardVisible = this.isCardVisible(lastCard);
this.setState({
canScrollNext: !lastCardVisible,
canScrollPrevious: !firstCardVisible,
});
};
const currentScrollLeft = cardRow.scrollLeft;
const direction = currentScrollLeft > scrollTarget ? 'left' : 'right';
this.scrollCardsAnimated(cardRow, scrollTarget, direction, animationCallback);
}
scrollCardsAnimated = (
cardRow: HTMLDivElement,
scrollTarget: number,
direction: string,
callback: () => any
) => {
let start;
const step = timestamp => {
if (!start) start = timestamp;
const currentLeftVal = cardRow.scrollLeft;
let newTarget;
let shouldContinue;
let progress = currentLeftVal;
if (direction === 'right') {
progress += timestamp - start;
newTarget = Math.min(progress, scrollTarget);
shouldContinue = newTarget < scrollTarget;
} else {
progress -= timestamp - start;
newTarget = Math.max(progress, scrollTarget);
shouldContinue = newTarget > scrollTarget;
}
cardRow.scrollLeft = newTarget; // eslint-disable-line no-param-reassign
if (shouldContinue) {
window.requestAnimationFrame(step);
} else {
callback();
}
};
window.requestAnimationFrame(step);
};
// check if a card is fully visible horizontally
isCardVisible = (section: HTMLElement) => {
const rect = section.getBoundingClientRect();
const isVisible = rect.left >= 0 && rect.right <= window.innerWidth;
return isVisible;
};
handleScrollNext() {
const cardRow = this.rowItems;
if (cardRow) {
const cards = cardRow.getElementsByTagName('section');
// loop over items until we find one that is on the screen
// continue searching until a card isn't fully visible, this is the new target
let firstFullVisibleCard;
let firstSemiVisibleCard;
for (let i = 0; i < cards.length; i += 1) {
const currentCardVisible = this.isCardVisible(cards[i]);
if (firstFullVisibleCard && !currentCardVisible) {
firstSemiVisibleCard = cards[i];
break;
} else if (currentCardVisible) {
[firstFullVisibleCard] = cards;
}
}
if (firstFullVisibleCard && firstSemiVisibleCard) {
const scrollTarget = firstSemiVisibleCard.offsetLeft - firstFullVisibleCard.offsetLeft;
this.handleScroll(cardRow, scrollTarget);
}
}
}
handleScrollPrevious() {
const cardRow = this.rowItems;
if (cardRow) {
const cards = cardRow.getElementsByTagName('section');
let hasFoundCard;
let numberOfCardsThatCanFit = 0;
// loop starting at the end until we find a visible card
// then count to find how many cards can fit on the screen
for (let i = cards.length - 1; i >= 0; i -= 1) {
const currentCard = cards[i];
const isCurrentCardVisible = this.isCardVisible(currentCard);
if (isCurrentCardVisible) {
if (!hasFoundCard) {
hasFoundCard = true;
}
numberOfCardsThatCanFit += 1;
} else if (hasFoundCard) {
// this card is off the screen to the left
// we know how many cards can fit on a screen
// find the new target and scroll
const firstCardOffsetLeft = cards[0].offsetLeft;
const cardIndexToScrollTo = i + 1 - numberOfCardsThatCanFit;
const newFirstCard = cards[cardIndexToScrollTo];
let scrollTarget;
if (newFirstCard) {
scrollTarget = newFirstCard.offsetLeft;
} else {
// more cards can fit on the screen than are currently hidden
// just scroll to the first card
scrollTarget = cards[0].offsetLeft;
}
scrollTarget -= firstCardOffsetLeft; // to play nice with the margins
this.handleScroll(cardRow, scrollTarget);
break;
}
}
}
}
render() {
const { category, names, categoryLink } = this.props;
const { canScrollNext, canScrollPrevious } = this.state;
// The lint was throwing an error saying we should use <button> instead of <a>
// We are using buttons, needs more exploration
return (
<div className="card-row">
<div className="card-row__header">
<div className="card-row__title">
<h3>
{categoryLink ? (
<Button
className="button-text no-underline"
label={category}
navigate="/show"
navigateParams={{ uri: categoryLink }}
/>
) : (
category
)}
</h3>
{category &&
category.match(/^community/i) && (
<ToolTip
label={__("What's this?")}
body={__(
'Community Content is a public space where anyone can share content with the rest of the LBRY community. Bid on the names "one," "two," "three," "four" and "five" to put your content here!'
)}
/>
)}
</div>
<div>
<Button
inverse
circle
disabled={!canScrollPrevious}
onClick={this.handleScrollPrevious}
icon="chevron-left"
/>
<Button
inverse
circle
disabled={!canScrollNext}
onClick={this.handleScrollNext}
icon="chevron-right"
/>
</div>
</div>
<div
ref={ref => {
this.rowItems = ref;
}}
className="card-row__scrollhouse"
>
{names &&
names.map(name => (
<FileCard key={name} displayStyle="card" uri={lbryuri.normalize(name)} />
))}
</div>
</div>
);
}
}
export default CategoryList;

View file

@ -0,0 +1,43 @@
// @flow
import React from 'react';
import classnames from 'classnames';
import * as icons from 'constants/icons';
type Props = {
icon: string,
fixed?: boolean,
padded?: boolean,
};
class Icon extends React.PureComponent<Props> {
getIconTitle() {
const { icon } = this.props;
switch (icon) {
case icons.FEATURED:
return __('Watch this and earn rewards.');
case icons.LOCAL:
return __('You have a copy of this file.');
default:
return '';
}
}
render() {
const { icon, fixed, padded } = this.props;
const iconClassName = icon.startsWith('icon-') ? icon : `icon-${icon}`;
const title = this.getIconTitle();
const spanClassName = classnames(
{
'icon--fixed-width': fixed,
'icon--padded': padded,
},
iconClassName
);
return <span className={spanClassName} title={title} />;
}
}
export default Icon;

View file

@ -0,0 +1,57 @@
// @flow
import React from 'react';
import classnames from 'classnames';
import Icon from 'component/common/icon';
import Button from 'component/link';
type Props = {
body: string,
label: string,
};
type State = {
showTooltip: boolean,
};
class ToolTip extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
showTooltip: false,
};
(this: any).handleClick = this.handleClick.bind(this);
}
handleClick() {
const { showTooltip } = this.state;
if (!showTooltip) {
document.addEventListener('click', this.handleClick);
} else {
document.removeEventListener('click', this.handleClick);
}
this.setState({
showTooltip: !showTooltip,
});
}
render() {
const { label, body } = this.props;
const { showTooltip } = this.state;
return (
<span className="tooltip">
<Button fakeLink className="help tooltip__link" onClick={this.handleClick}>
{label}
{showTooltip && <Icon icon="times" fixed />}
</Button>
<div className={classnames('tooltip__body', { hidden: !showTooltip })}>{body}</div>
</span>
);
}
}
export default ToolTip;

View file

@ -1,111 +1,100 @@
// @flow
import React from 'react'; import React from 'react';
import lbryuri from 'lbryuri.js'; import lbryuri from 'lbryuri';
import CardMedia from 'component/cardMedia'; import CardMedia from 'component/cardMedia';
import Link from 'component/link';
import { TruncatedText } from 'component/common'; import { TruncatedText } from 'component/common';
import Icon from 'component/icon'; import Icon from 'component/common/icon';
import FilePrice from 'component/filePrice'; import FilePrice from 'component/filePrice';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import NsfwOverlay from 'component/nsfwOverlay'; import NsfwOverlay from 'component/nsfwOverlay';
import TruncatedMarkdown from 'component/truncatedMarkdown';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import classnames from 'classnames';
class FileCard extends React.PureComponent { type Props = {
constructor(props) { isResolvingUri: boolean,
super(props); resolveUri: string => void,
uri: string,
this.state = { claim: ?{ claim_id: string },
hovered: false, fileInfo: ?{},
}; metadata: ?{ nsfw: boolean, thumbnail: ?string },
} navigate: (string, ?{}) => void,
rewardedContentClaimIds: Array<string>,
obscureNsfw: boolean,
};
class FileCard extends React.PureComponent<Props> {
componentWillMount() { componentWillMount() {
this.resolve(this.props); this.resolve(this.props);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps: Props) {
this.resolve(nextProps); this.resolve(nextProps);
} }
resolve(props) { resolve = (props: Props) => {
const { isResolvingUri, resolveUri, claim, uri } = props; const { isResolvingUri, resolveUri, claim, uri } = props;
if (!isResolvingUri && claim === undefined && uri) { if (!isResolvingUri && claim === undefined && uri) {
resolveUri(uri); resolveUri(uri);
} }
} };
handleMouseOver() {
this.setState({
hovered: true,
});
}
handleMouseOut() {
this.setState({
hovered: false,
});
}
render() { render() {
const { const {
claim, claim,
fileInfo, fileInfo,
metadata, metadata,
isResolvingUri,
navigate, navigate,
rewardedContentClaimIds, rewardedContentClaimIds,
obscureNsfw,
} = this.props; } = this.props;
const uri = lbryuri.normalize(this.props.uri); const uri = lbryuri.normalize(this.props.uri);
const title = metadata && metadata.title ? metadata.title : uri; const title = metadata && metadata.title ? metadata.title : uri;
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null; const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null;
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw;
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id); const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
let description = ''; // Come back to this on other pages
if (isResolvingUri && !claim) { // let description = '';
description = __('Loading...'); // if (isResolvingUri && !claim) {
} else if (metadata && metadata.description) { // description = __('Loading...');
description = metadata.description; // } else if (metadata && metadata.description) {
} else if (claim === null) { // description = metadata.description;
description = __('This address contains no content.'); // } else if (claim === null) {
} // description = __('This address contains no content.');
// }
// We don't want to allow a click handler unless it's in focus
// I'll come back to this when I work on site-wide keyboard navigation
/* eslint-disable jsx-a11y/click-events-have-key-events */
return ( return (
<section <section
className={`card card--small card--link ${obscureNsfw ? 'card--obscured ' : ''}`} tabIndex="0"
onMouseEnter={this.handleMouseOver.bind(this)} role="button"
onMouseLeave={this.handleMouseOut.bind(this)} onClick={() => navigate('/show', { uri })}
className={classnames('card card--small card__link', {
'card--obscured': shouldObscureNsfw,
})}
> >
<div className="card__inner"> <CardMedia thumbnail={thumbnail} />
<Link onClick={() => navigate('/show', { uri })} className="card__link">
<CardMedia title={title} thumbnail={thumbnail} />
<div className="card__title-identity"> <div className="card__title-identity">
<div className="card__title" title={title}> <div className="card__title--small">
<TruncatedText lines={1}>{title}</TruncatedText> <TruncatedText lines={3}>{title}</TruncatedText>
</div> </div>
<div className="card__subtitle"> <div className="card__subtitle">
<span className="card__indicators card--file-subtitle"> <UriIndicator uri={uri} link />
<FilePrice uri={uri} />{' '} <div className="card--file-subtitle">
{isRewardContent && <Icon icon={icons.FEATURED} leftPad />}{' '} <FilePrice uri={uri} /> {isRewardContent && <Icon icon={icons.FEATURED} padded />}
{fileInfo && <Icon icon={icons.LOCAL} leftPad />} {fileInfo && <Icon icon={icons.LOCAL} padded />}
</span>
<span className="card--file-subtitle">
<UriIndicator uri={uri} link span smallCard />
</span>
</div> </div>
</div> </div>
</Link>
{/* Test for nizuka's design: should we remove description?
<div className="card__content card__subtext card__subtext--two-lines">
<TruncatedMarkdown lines={2}>{description}</TruncatedMarkdown>
</div> </div>
*/} {obscureNsfw && <NsfwOverlay />}
</div>
{obscureNsfw && this.state.hovered && <NsfwOverlay />}
</section> </section>
); );
/* eslint-enable jsx-a11y/click-events-have-key-events */
} }
} }

View file

@ -1,3 +1,4 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import lbryuri from 'lbryuri.js'; import lbryuri from 'lbryuri.js';
@ -5,7 +6,7 @@ import CardMedia from 'component/cardMedia';
import { TruncatedText } from 'component/common.js'; import { TruncatedText } from 'component/common.js';
import FilePrice from 'component/filePrice'; import FilePrice from 'component/filePrice';
import NsfwOverlay from 'component/nsfwOverlay'; import NsfwOverlay from 'component/nsfwOverlay';
import Icon from 'component/icon'; import Icon from 'component/common/icon';
class FileTile extends React.PureComponent { class FileTile extends React.PureComponent {
static SHOW_EMPTY_PUBLISH = 'publish'; static SHOW_EMPTY_PUBLISH = 'publish';
@ -133,3 +134,4 @@ class FileTile extends React.PureComponent {
} }
export default FileTile; export default FileTile;
/* eslint-enable */

View file

@ -1,8 +1,20 @@
// @flow
import React from 'react'; import React from 'react';
import Link from 'component/link'; import Button from 'component/link';
import WunderBar from 'component/wunderbar'; import WunderBar from 'component/wunderbar';
export const Header = props => { type Props = {
balance: string,
back: any => void,
forward: any => void,
isBackDisabled: boolean,
isForwardDisabled: boolean,
isUpgradeAvailable: boolean,
navigate: any => void,
downloadUpgrade: any => void,
};
export const Header = (props: Props) => {
const { const {
balance, balance,
back, back,
@ -15,85 +27,57 @@ export const Header = props => {
} = props; } = props;
return ( return (
<header id="header"> <header id="header">
<div className="header__item"> <div className="header__actions-left">
<Link <Button
alt
circle
onClick={back} onClick={back}
disabled={isBackDisabled} disabled={isBackDisabled}
button="alt button--flat" icon="arrow-left"
icon="icon-arrow-left" description={__('Navigate back')}
title={__('Back')}
/> />
</div>
<div className="header__item"> <Button
<Link alt
circle
onClick={forward} onClick={forward}
disabled={isForwardDisabled} disabled={isForwardDisabled}
button="alt button--flat" icon="arrow-right"
icon="icon-arrow-right" description={__('Navigate forward')}
title={__('Forward')}
/> />
<Button alt onClick={() => navigate('/discover')} icon="home" description={__('Home')} />
</div> </div>
<div className="header__item">
<Link
onClick={() => navigate('/discover')}
button="alt button--flat"
icon="icon-home"
title={__('Discover Content')}
/>
</div>
<div className="header__item">
<Link
onClick={() => navigate('/subscriptions')}
button="alt button--flat"
icon="icon-at"
title={__('My Subscriptions')}
/>
</div>
<div className="header__item header__item--wunderbar">
<WunderBar /> <WunderBar />
</div>
<div className="header__item"> <div className="header__actions-right">
<Link <Button
inverse
onClick={() => navigate('/wallet')} onClick={() => navigate('/wallet')}
button="text" icon="user"
className="no-underline" label={isUpgradeAvailable ? `${balance} LBC` : `You have ${balance} LBC`}
icon="icon-bank"
label={balance}
title={__('Wallet')} title={__('Wallet')}
/> />
</div>
<div className="header__item"> <Button
<Link
onClick={() => navigate('/publish')} onClick={() => navigate('/publish')}
button="primary button--flat" icon="cloud-upload"
icon="icon-upload" label={isUpgradeAvailable ? '' : __('Publish')}
label={__('Publish')}
/> />
</div>
<div className="header__item"> <Button alt onClick={() => navigate('/settings')} icon="gear" title={__('Settings')} />
<Link
onClick={() => navigate('/downloaded')} <Button alt onClick={() => navigate('/help')} icon="question" title={__('Help')} />
button="alt button--flat"
icon="icon-folder"
title={__('Downloads and Publishes')}
/>
</div>
<div className="header__item">
<Link
onClick={() => navigate('/settings')}
button="alt button--flat"
icon="icon-gear"
title={__('Settings')}
/>
</div>
{isUpgradeAvailable && ( {isUpgradeAvailable && (
<Link <Button
onClick={() => downloadUpgrade()} onClick={() => downloadUpgrade()}
button="primary button--flat" icon="arrow-up"
icon="icon-arrow-up"
label={__('Upgrade App')} label={__('Upgrade App')}
title={__('Upgrade app')}
/> />
)} )}
</div>
</header> </header>
); );
}; };

View file

@ -1,5 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import Icon from './view';
export default connect(null, null)(Icon);

View file

@ -1,50 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import * as icons from 'constants/icons';
import classnames from 'classnames';
export default class Icon extends React.PureComponent {
static propTypes = {
icon: PropTypes.string.isRequired,
fixed: PropTypes.bool,
};
static defaultProps = {
fixed: false,
};
getIconClass() {
const { icon } = this.props;
return icon.startsWith('icon-') ? icon : `icon-${icon}`;
}
getIconTitle() {
switch (this.props.icon) {
case icons.FEATURED:
return __('Watch this and earn rewards.');
case icons.LOCAL:
return __('You have a copy of this file.');
default:
return '';
}
}
render() {
const { icon, fixed, className, leftPad } = this.props;
const iconClass = this.getIconClass();
const title = this.getIconTitle();
const spanClassName = classnames(
'icon',
iconClass,
{
'icon-fixed-width': fixed,
'icon--left-pad': leftPad,
},
className
);
return <span className={spanClassName} title={title} />;
}
}

View file

@ -1,60 +1,99 @@
import React from 'react'; // @flow
import Icon from 'component/icon'; import * as React from 'react';
import Icon from 'component/common/icon';
import classnames from 'classnames';
const Link = props => { type Props = {
onClick: ?(any) => any,
href: ?string,
title: ?string,
label: ?string,
icon: ?string,
iconRight: ?string,
disabled: ?boolean,
children: ?React.Node,
navigate: ?string,
// TODO: these (nav) should be a reusable type
doNavigate: (string, ?any) => void,
navigateParams: any,
className: ?string,
inverse: ?boolean,
circle: ?boolean,
alt: ?boolean,
flat: ?boolean,
fakeLink: ?boolean,
description: ?string,
};
const Button = (props: Props) => {
const { const {
onClick,
href, href,
title, title,
style,
label, label,
icon, icon,
iconRight, iconRight,
button,
disabled, disabled,
children, children,
navigate, navigate,
navigateParams, navigateParams,
doNavigate, doNavigate,
className, className,
span, inverse,
alt,
circle,
flat,
fakeLink,
description,
...otherProps
} = props; } = props;
const combinedClassName = const combinedClassName = classnames(
(className || '') + {
(!className && !button ? 'button-text' : '') + // Non-button links get the same look as text buttons btn: !fakeLink,
(button ? ` button-block button-${button} button-set-item` : '') + 'btn--link': fakeLink,
(disabled ? ' disabled' : ''); 'btn--primary': !fakeLink && !alt,
'btn--alt': alt,
'btn--inverse': inverse,
'btn--disabled': disabled,
'btn--circle': circle,
'btn--flat': flat,
},
className
);
const onClick = const extendedOnClick =
!props.onClick && navigate !onClick && navigate
? event => { ? event => {
event.stopPropagation(); event.stopPropagation();
doNavigate(navigate, navigateParams || {}); doNavigate(navigate, navigateParams || {});
} }
: props.onClick; : onClick;
let content; const content = (
if (children) { <React.Fragment>
content = children; {icon && <Icon icon={icon} fixed />}
} else { {label && <span className="btn__label">{label}</span>}
content = ( {children && children}
<span {...('button' in props ? { className: 'button__content' } : {})}> {iconRight && <Icon icon={iconRight} fixed />}
{icon ? <Icon icon={icon} fixed /> : null} </React.Fragment>
{label ? <span className="link-label">{label}</span> : null}
{iconRight ? <Icon icon={iconRight} fixed /> : null}
</span>
); );
}
const linkProps = { return href ? (
className: combinedClassName, <a className={combinedClassName} href={href} title={title}>
href: href || 'javascript:;', {content}
title, </a>
onClick, ) : (
style, <button
}; aria-label={description || title}
className={combinedClassName}
return span ? <span {...linkProps}>{content}</span> : <a {...linkProps}>{content}</a>; onClick={extendedOnClick}
disabled={disabled}
{...otherProps}
>
{content}
</button>
);
}; };
export default Link; export default Button;

View file

@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import { selectPageTitle } from 'redux/selectors/navigation';
import Page from './view';
const select = state => ({
title: selectPageTitle(state),
});
export default connect(select, null)(Page);

View file

@ -0,0 +1,26 @@
// @flow
import * as React from 'react';
import classnames from 'classnames';
import { BusyMessage } from 'component/common';
type Props = {
children: React.Node,
title: ?string,
noPadding: ?boolean,
isLoading: ?boolean,
};
const Page = (props: Props) => {
const { children, title, noPadding, isLoading } = props;
return (
<main id="main-content">
<div className="page__header">
{title && <h1 className="page__title">{title}</h1>}
{isLoading && <BusyMessage message={__('Fetching content')} />}
</div>
<div className={classnames('main', { 'main--no-padding': noPadding })}>{children}</div>
</main>
);
};
export default Page;

View file

@ -1,54 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
export class ToolTip extends React.PureComponent {
static propTypes = {
body: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
};
constructor(props) {
super(props);
this.state = {
showTooltip: false,
};
}
handleClick() {
this.setState({
showTooltip: !this.state.showTooltip,
});
}
handleTooltipMouseOut() {
this.setState({
showTooltip: false,
});
}
render() {
return (
<span className={`tooltip ${this.props.className || ''}`}>
<a
className="tooltip__link"
onClick={() => {
this.handleClick();
}}
>
{this.props.label}
</a>
<div
className={`tooltip__body ${this.state.showTooltip ? '' : ' hidden'}`}
onMouseOut={() => {
this.handleTooltipMouseOut();
}}
>
{this.props.body}
</div>
</span>
);
}
}
export default ToolTip;

View file

@ -1,35 +1,46 @@
// @flow
import React from 'react'; import React from 'react';
import Icon from 'component/icon'; import { Icon } from 'component/common';
import Link from 'component/link'; import Button from 'component/link';
import lbryuri from 'lbryuri'; import lbryuri from 'lbryuri';
import classnames from 'classnames'; import classnames from 'classnames';
class UriIndicator extends React.PureComponent { type Props = {
isResolvingUri: boolean,
resolveUri: string => void,
claim: {
channel_name: string,
has_signature: boolean,
signature_is_valid: boolean,
value: {
publisherSignature: { certificateId: string },
},
},
uri: string,
link: ?boolean,
};
class UriIndicator extends React.PureComponent<Props> {
componentWillMount() { componentWillMount() {
this.resolve(this.props); this.resolve(this.props);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps: Props) {
this.resolve(nextProps); this.resolve(nextProps);
} }
resolve(props) { resolve = (props: Props) => {
const { isResolvingUri, resolveUri, claim, uri } = props; const { isResolvingUri, resolveUri, claim, uri } = props;
if (!isResolvingUri && claim === undefined && uri) { if (!isResolvingUri && claim === undefined && uri) {
resolveUri(uri); resolveUri(uri);
} }
} };
render() { render() {
const { claim, link, uri, isResolvingUri, smallCard, span } = this.props; const { claim, link, isResolvingUri } = this.props;
if (isResolvingUri && !claim) {
return <span className="empty">Validating...</span>;
}
if (!claim) { if (!claim) {
return <span className="empty">Unused</span>; return <span className="empty">{isResolvingUri ? 'Validating...' : 'Unused'}</span>;
} }
const { const {
@ -38,14 +49,17 @@ class UriIndicator extends React.PureComponent {
signature_is_valid: signatureIsValid, signature_is_valid: signatureIsValid,
value, value,
} = claim; } = claim;
const channelClaimId = const channelClaimId =
value && value.publisherSignature && value.publisherSignature.certificateId; value && value.publisherSignature && value.publisherSignature.certificateId;
if (!hasSignature || !channelName) { if (!hasSignature || !channelName) {
return <span className="empty">Anonymous</span>; return <span>Anonymous</span>;
} }
let icon, channelLink, modifier; let icon;
let channelLink;
let modifier;
if (signatureIsValid) { if (signatureIsValid) {
modifier = 'valid'; modifier = 'valid';
@ -59,7 +73,6 @@ class UriIndicator extends React.PureComponent {
<span> <span>
<span <span
className={classnames('channel-name', { className={classnames('channel-name', {
'channel-name--small': smallCard,
'button-text no-underline': link, 'button-text no-underline': link,
})} })}
> >
@ -81,14 +94,9 @@ class UriIndicator extends React.PureComponent {
} }
return ( return (
<Link <Button navigate="/show" navigateParams={{ uri: channelLink }} fakeLink>
navigate="/show"
navigateParams={{ uri: channelLink }}
className="no-underline"
span={span}
>
{inner} {inner}
</Link> </Button>
); );
} }
} }

View file

@ -1,19 +1,21 @@
import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import lbryuri from 'lbryuri.js'; import lbryuri from 'lbryuri';
import { selectWunderBarAddress, selectWunderBarIcon } from 'redux/selectors/search'; import { selectState as selectSearch, selectWunderBarAddress } from 'redux/selectors/search';
import { doNavigate } from 'redux/actions/navigation'; import { doNavigate } from 'redux/actions/navigation';
import { updateSearchQuery, getSearchSuggestions } from 'redux/actions/search';
import Wunderbar from './view'; import Wunderbar from './view';
const select = state => ({ const select = state => ({
...selectSearch(state),
address: selectWunderBarAddress(state), address: selectWunderBarAddress(state),
icon: selectWunderBarIcon(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
onSearch: query => dispatch(doNavigate('/search', { query })), onSearch: query => dispatch(doNavigate('/search', { query })),
onSubmit: (query, extraParams) => onSubmit: (query, extraParams) =>
dispatch(doNavigate('/show', { uri: lbryuri.normalize(query), ...extraParams })), dispatch(doNavigate('/show', { uri: lbryuri.normalize(query), ...extraParams })),
updateSearchQuery: query => dispatch(updateSearchQuery(query)),
getSearchSuggestions: query => dispatch(getSearchSuggestions(query)),
}); });
export default connect(select, perform)(Wunderbar); export default connect(select, perform)(Wunderbar);

View file

@ -0,0 +1,601 @@
/*
This is taken from https://github.com/reactjs/react-autocomplete
We aren't using that component because (for now) there is no way to autohightlight
the first item if it isn't an exact match from what is in the search bar.
Our use case is:
value in search bar: "hello"
first suggestion: "lbry://hello"
I changed the function maybeAutoCompleteText to check if the suggestion contains
the search query anywhere, instead of the suggestion starting with it
https://github.com/reactjs/react-autocomplete/issues/239
*/
/* eslint-disable */
const React = require('react');
const PropTypes = require('prop-types');
const { findDOMNode } = require('react-dom');
const scrollIntoView = require('dom-scroll-into-view');
const IMPERATIVE_API = [
'blur',
'checkValidity',
'click',
'focus',
'select',
'setCustomValidity',
'setSelectionRange',
'setRangeText',
];
function getScrollOffset() {
return {
x:
window.pageXOffset !== undefined
? window.pageXOffset
: (document.documentElement || document.body.parentNode || document.body).scrollLeft,
y:
window.pageYOffset !== undefined
? window.pageYOffset
: (document.documentElement || document.body.parentNode || document.body).scrollTop,
};
}
export default class Autocomplete extends React.Component {
static propTypes = {
/**
* The items to display in the dropdown menu
*/
items: PropTypes.array.isRequired,
/**
* The value to display in the input field
*/
value: PropTypes.any,
/**
* Arguments: `event: Event, value: String`
*
* Invoked every time the user changes the input's value.
*/
onChange: PropTypes.func,
/**
* Arguments: `value: String, item: Any`
*
* Invoked when the user selects an item from the dropdown menu.
*/
onSelect: PropTypes.func,
/**
* Arguments: `item: Any, value: String`
*
* Invoked for each entry in `items` and its return value is used to
* determine whether or not it should be displayed in the dropdown menu.
* By default all items are always rendered.
*/
shouldItemRender: PropTypes.func,
/**
* Arguments: `itemA: Any, itemB: Any, value: String`
*
* The function which is used to sort `items` before display.
*/
sortItems: PropTypes.func,
/**
* Arguments: `item: Any`
*
* Used to read the display value from each entry in `items`.
*/
getItemValue: PropTypes.func.isRequired,
/**
* Arguments: `item: Any, isHighlighted: Boolean, styles: Object`
*
* Invoked for each entry in `items` that also passes `shouldItemRender` to
* generate the render tree for each item in the dropdown menu. `styles` is
* an optional set of styles that can be applied to improve the look/feel
* of the items in the dropdown menu.
*/
renderItem: PropTypes.func.isRequired,
/**
* Arguments: `items: Array<Any>, value: String, styles: Object`
*
* Invoked to generate the render tree for the dropdown menu. Ensure the
* returned tree includes every entry in `items` or else the highlight order
* and keyboard navigation logic will break. `styles` will contain
* { top, left, minWidth } which are the coordinates of the top-left corner
* and the width of the dropdown menu.
*/
renderMenu: PropTypes.func,
/**
* Styles that are applied to the dropdown menu in the default `renderMenu`
* implementation. If you override `renderMenu` and you want to use
* `menuStyle` you must manually apply them (`this.props.menuStyle`).
*/
menuStyle: PropTypes.object,
/**
* Arguments: `props: Object`
*
* Invoked to generate the input element. The `props` argument is the result
* of merging `props.inputProps` with a selection of props that are required
* both for functionality and accessibility. At the very least you need to
* apply `props.ref` and all `props.on<event>` event handlers. Failing to do
* this will cause `Autocomplete` to behave unexpectedly.
*/
renderInput: PropTypes.func,
/**
* Props passed to `props.renderInput`. By default these props will be
* applied to the `<input />` element rendered by `Autocomplete`, unless you
* have specified a custom value for `props.renderInput`. Any properties
* supported by `HTMLInputElement` can be specified, apart from the
* following which are set by `Autocomplete`: value, autoComplete, role,
* aria-autocomplete. `inputProps` is commonly used for (but not limited to)
* placeholder, event handlers (onFocus, onBlur, etc.), autoFocus, etc..
*/
inputProps: PropTypes.object,
/**
* Props that are applied to the element which wraps the `<input />` and
* dropdown menu elements rendered by `Autocomplete`.
*/
wrapperProps: PropTypes.object,
/**
* This is a shorthand for `wrapperProps={{ style: <your styles> }}`.
* Note that `wrapperStyle` is applied before `wrapperProps`, so the latter
* will win if it contains a `style` entry.
*/
wrapperStyle: PropTypes.object,
/**
* Whether or not to automatically highlight the top match in the dropdown
* menu.
*/
autoHighlight: PropTypes.bool,
/**
* Whether or not to automatically select the highlighted item when the
* `<input>` loses focus.
*/
selectOnBlur: PropTypes.bool,
/**
* Arguments: `isOpen: Boolean`
*
* Invoked every time the dropdown menu's visibility changes (i.e. every
* time it is displayed/hidden).
*/
onMenuVisibilityChange: PropTypes.func,
/**
* Used to override the internal logic which displays/hides the dropdown
* menu. This is useful if you want to force a certain state based on your
* UX/business logic. Use it together with `onMenuVisibilityChange` for
* fine-grained control over the dropdown menu dynamics.
*/
open: PropTypes.bool,
debug: PropTypes.bool,
};
static defaultProps = {
value: '',
wrapperProps: {},
wrapperStyle: {
display: 'inline-block',
},
inputProps: {},
renderInput(props) {
return <input {...props} />;
},
onChange() {},
onSelect() {},
renderMenu(items, value, style) {
return <div style={{ ...style, ...this.menuStyle }} children={items} />;
},
menuStyle: {
borderRadius: '3px',
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)',
background: 'rgba(255, 255, 255, 0.9)',
padding: '2px 0',
fontSize: '90%',
position: 'fixed',
overflow: 'auto',
maxHeight: '50%', // TODO: don't cheat, let it flow to the bottom,
},
autoHighlight: true,
selectOnBlur: false,
onMenuVisibilityChange() {},
};
constructor(props) {
super(props);
this.state = {
isOpen: false,
highlightedIndex: null,
};
this._debugStates = [];
this.ensureHighlightedIndex = this.ensureHighlightedIndex.bind(this);
this.exposeAPI = this.exposeAPI.bind(this);
this.handleInputFocus = this.handleInputFocus.bind(this);
this.handleInputBlur = this.handleInputBlur.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleInputClick = this.handleInputClick.bind(this);
this.maybeAutoCompleteText = this.maybeAutoCompleteText.bind(this);
}
componentWillMount() {
// this.refs is frozen, so we need to assign a new object to it
this.refs = {};
this._ignoreBlur = false;
this._ignoreFocus = false;
this._scrollOffset = null;
this._scrollTimer = null;
}
componentWillUnmount() {
clearTimeout(this._scrollTimer);
this._scrollTimer = null;
}
componentWillReceiveProps(nextProps) {
if (this.state.highlightedIndex !== null) {
this.setState(this.ensureHighlightedIndex);
}
if (
nextProps.autoHighlight &&
(this.props.value !== nextProps.value || this.state.highlightedIndex === null)
) {
this.setState(this.maybeAutoCompleteText);
}
}
componentDidMount() {
if (this.isOpen()) {
this.setMenuPositions();
}
}
componentDidUpdate(prevProps, prevState) {
if (
(this.state.isOpen && !prevState.isOpen) ||
('open' in this.props && this.props.open && !prevProps.open)
)
this.setMenuPositions();
this.maybeScrollItemIntoView();
if (prevState.isOpen !== this.state.isOpen) {
this.props.onMenuVisibilityChange(this.state.isOpen);
}
}
exposeAPI(el) {
this.refs.input = el;
IMPERATIVE_API.forEach(ev => (this[ev] = el && el[ev] && el[ev].bind(el)));
}
maybeScrollItemIntoView() {
if (this.isOpen() && this.state.highlightedIndex !== null) {
const itemNode = this.refs[`item-${this.state.highlightedIndex}`];
const menuNode = this.refs.menu;
scrollIntoView(findDOMNode(itemNode), findDOMNode(menuNode), {
onlyScrollIfNeeded: true,
});
}
}
handleKeyDown(event) {
if (Autocomplete.keyDownHandlers[event.key])
Autocomplete.keyDownHandlers[event.key].call(this, event);
else if (!this.isOpen()) {
this.setState({
isOpen: true,
});
}
}
handleChange(event) {
this.props.onChange(event, event.target.value);
}
static keyDownHandlers = {
ArrowDown(event) {
event.preventDefault();
const itemsLength = this.getFilteredItems(this.props).length;
if (!itemsLength) return;
const { highlightedIndex } = this.state;
const index =
highlightedIndex === null || highlightedIndex === itemsLength - 1
? 0
: highlightedIndex + 1;
this.setState({
highlightedIndex: index,
isOpen: true,
});
},
ArrowUp(event) {
event.preventDefault();
const itemsLength = this.getFilteredItems(this.props).length;
if (!itemsLength) return;
const { highlightedIndex } = this.state;
const index =
highlightedIndex === 0 || highlightedIndex === null
? itemsLength - 1
: highlightedIndex - 1;
this.setState({
highlightedIndex: index,
isOpen: true,
});
},
Enter(event) {
// Key code 229 is used for selecting items from character selectors (Pinyin, Kana, etc)
if (event.keyCode !== 13) return;
if (!this.isOpen()) {
// menu is closed so there is no selection to accept -> do nothing
} else if (this.state.highlightedIndex == null) {
// input has focus but no menu item is selected + enter is hit -> close the menu, highlight whatever's in input
this.setState(
{
isOpen: false,
},
() => {
this.refs.input.select();
}
);
} else {
// text entered + menu item has been highlighted + enter is hit -> update value to that of selected menu item, close the menu
event.preventDefault();
const item = this.getFilteredItems(this.props)[this.state.highlightedIndex];
const value = this.props.getItemValue(item);
this.setState(
{
isOpen: false,
highlightedIndex: null,
},
() => {
// this.refs.input.focus() // TODO: file issue
this.refs.input.setSelectionRange(value.length, value.length);
this.props.onSelect(value, item);
}
);
}
},
Escape() {
// In case the user is currently hovering over the menu
this.setIgnoreBlur(false);
this.setState({
highlightedIndex: null,
isOpen: false,
});
},
Tab() {
// In case the user is currently hovering over the menu
this.setIgnoreBlur(false);
},
};
getFilteredItems(props) {
let items = props.items;
if (props.shouldItemRender) {
items = items.filter(item => props.shouldItemRender(item, props.value));
}
if (props.sortItems) {
items.sort((a, b) => props.sortItems(a, b, props.value));
}
return items;
}
maybeAutoCompleteText(state, props) {
const { highlightedIndex } = state;
const { value, getItemValue } = props;
const index = highlightedIndex === null ? 0 : highlightedIndex;
const matchedItem = this.getFilteredItems(props)[index];
if (value !== '' && matchedItem) {
const itemValue = getItemValue(matchedItem);
const itemValueDoesMatch =
itemValue.toLowerCase().indexOf(
value.toLowerCase()
// below line is the the only thing that is changed from the real component
) !== -1;
if (itemValueDoesMatch) {
return { highlightedIndex: index };
}
}
return { highlightedIndex: null };
}
ensureHighlightedIndex(state, props) {
if (state.highlightedIndex >= this.getFilteredItems(props).length) {
return { highlightedIndex: null };
}
}
setMenuPositions() {
const node = this.refs.input;
const rect = node.getBoundingClientRect();
const computedStyle = global.window.getComputedStyle(node);
const marginBottom = parseInt(computedStyle.marginBottom, 10) || 0;
const marginLeft = parseInt(computedStyle.marginLeft, 10) || 0;
const marginRight = parseInt(computedStyle.marginRight, 10) || 0;
this.setState({
menuTop: rect.bottom + marginBottom,
menuLeft: rect.left + marginLeft,
menuWidth: rect.width + marginLeft + marginRight,
});
}
highlightItemFromMouse(index) {
this.setState({ highlightedIndex: index });
}
selectItemFromMouse(item) {
const value = this.props.getItemValue(item);
// The menu will de-render before a mouseLeave event
// happens. Clear the flag to release control over focus
this.setIgnoreBlur(false);
this.setState(
{
isOpen: false,
highlightedIndex: null,
},
() => {
this.props.onSelect(value, item);
}
);
}
setIgnoreBlur(ignore) {
this._ignoreBlur = ignore;
}
renderMenu() {
const items = this.getFilteredItems(this.props).map((item, index) => {
const element = this.props.renderItem(item, this.state.highlightedIndex === index, {
cursor: 'default',
});
return React.cloneElement(element, {
onMouseEnter: () => this.highlightItemFromMouse(index),
onClick: () => this.selectItemFromMouse(item),
ref: e => (this.refs[`item-${index}`] = e),
});
});
const style = {
left: this.state.menuLeft,
top: this.state.menuTop,
minWidth: this.state.menuWidth,
};
const menu = this.props.renderMenu(items, this.props.value, style);
return React.cloneElement(menu, {
ref: e => (this.refs.menu = e),
// Ignore blur to prevent menu from de-rendering before we can process click
onMouseEnter: () => this.setIgnoreBlur(true),
onMouseLeave: () => this.setIgnoreBlur(false),
});
}
handleInputBlur(event) {
if (this._ignoreBlur) {
this._ignoreFocus = true;
this._scrollOffset = getScrollOffset();
this.refs.input.focus();
return;
}
let setStateCallback;
const { highlightedIndex } = this.state;
if (this.props.selectOnBlur && highlightedIndex !== null) {
const items = this.getFilteredItems(this.props);
const item = items[highlightedIndex];
const value = this.props.getItemValue(item);
setStateCallback = () => this.props.onSelect(value, item);
}
this.setState(
{
isOpen: false,
highlightedIndex: null,
},
setStateCallback
);
const { onBlur } = this.props.inputProps;
if (onBlur) {
onBlur(event);
}
}
handleInputFocus(event) {
if (this._ignoreFocus) {
this._ignoreFocus = false;
const { x, y } = this._scrollOffset;
this._scrollOffset = null;
// Focus will cause the browser to scroll the <input> into view.
// This can cause the mouse coords to change, which in turn
// could cause a new highlight to happen, cancelling the click
// event (when selecting with the mouse)
window.scrollTo(x, y);
// Some browsers wait until all focus event handlers have been
// processed before scrolling the <input> into view, so let's
// scroll again on the next tick to ensure we're back to where
// the user was before focus was lost. We could do the deferred
// scroll only, but that causes a jarring split second jump in
// some browsers that scroll before the focus event handlers
// are triggered.
clearTimeout(this._scrollTimer);
this._scrollTimer = setTimeout(() => {
this._scrollTimer = null;
window.scrollTo(x, y);
}, 0);
return;
}
this.setState({ isOpen: true });
const { onFocus } = this.props.inputProps;
if (onFocus) {
onFocus(event);
}
}
isInputFocused() {
const el = this.refs.input;
return el.ownerDocument && el === el.ownerDocument.activeElement;
}
handleInputClick() {
// Input will not be focused if it's disabled
if (this.isInputFocused() && !this.isOpen()) this.setState({ isOpen: true });
}
composeEventHandlers(internal, external) {
return external
? e => {
internal(e);
external(e);
}
: internal;
}
isOpen() {
return 'open' in this.props ? this.props.open : this.state.isOpen;
}
render() {
if (this.props.debug) {
// you don't like it, you love it
this._debugStates.push({
id: this._debugStates.length,
state: this.state,
});
}
const { inputProps, items } = this.props;
const open = this.isOpen();
return (
<div style={{ ...this.props.wrapperStyle }} {...this.props.wrapperProps}>
{this.props.renderInput({
...inputProps,
role: 'combobox',
'aria-autocomplete': 'list',
'aria-expanded': open,
autoComplete: 'off',
ref: this.exposeAPI,
onFocus: this.handleInputFocus,
onBlur: this.handleInputBlur,
onChange: this.handleChange,
onKeyDown: this.composeEventHandlers(this.handleKeyDown, inputProps.onKeyDown),
onClick: this.composeEventHandlers(this.handleInputClick, inputProps.onClick),
value: this.props.value,
})}
{open && !!items.length && this.renderMenu()}
{this.props.debug && (
<pre style={{ marginLeft: 300 }}>
{JSON.stringify(
this._debugStates.slice(
Math.max(0, this._debugStates.length - 5),
this._debugStates.length
),
null,
2
)}
</pre>
)}
</div>
);
}
}

View file

@ -1,166 +1,116 @@
// @flow
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import lbryuri from 'lbryuri';
import lbryuri from 'lbryuri.js'; import classnames from 'classnames';
import Icon from 'component/icon'; import Autocomplete from './internal/autocomplete';
import { parseQueryParams } from 'util/query_params';
class WunderBar extends React.PureComponent { type Props = {
static TYPING_TIMEOUT = 800; updateSearchQuery: string => void,
getSearchSuggestions: string => void,
onSearch: string => void,
onSubmit: string => void,
searchQuery: ?string,
isActive: boolean,
address: ?string,
suggestions: Array<string>,
};
static propTypes = { class WunderBar extends React.PureComponent<Props> {
onSearch: PropTypes.func.isRequired, constructor() {
onSubmit: PropTypes.func.isRequired, super();
}; (this: any).handleSubmit = this.handleSubmit.bind(this);
(this: any).handleChange = this.handleChange.bind(this);
constructor(props) { (this: any).focus = this.focus.bind(this);
super(props); this.input = undefined;
this._userTypingTimer = null;
this._isSearchDispatchPending = false;
this._input = null;
this._stateBeforeSearch = null;
this._resetOnNextBlur = true;
this.onChange = this.onChange.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.onReceiveRef = this.onReceiveRef.bind(this);
this.state = {
address: this.props.address,
icon: this.props.icon,
};
} }
componentWillUnmount() { input: ?HTMLInputElement;
if (this.userTypingTimer) {
clearTimeout(this._userTypingTimer); handleChange(e: SyntheticInputEvent<*>) {
const { updateSearchQuery, getSearchSuggestions } = this.props;
const { value } = e.target;
updateSearchQuery(value);
getSearchSuggestions(value);
}
focus() {
const { input } = this;
if (input) {
input.focus();
} }
} }
onChange(event) { handleSubmit(value: string) {
if (this._userTypingTimer) { if (!value) {
clearTimeout(this._userTypingTimer); return;
} }
this.setState({ address: event.target.value }); const { onSubmit, onSearch } = this.props;
this._isSearchDispatchPending = true; // if they choose the "search for {value}" in the suggestions
// it will contain the {query}?search
const choseDoSuggestedSearch = value.endsWith('?search');
const searchQuery = event.target.value; let searchValue = value;
if (choseDoSuggestedSearch) {
this._userTypingTimer = setTimeout(() => { searchValue = value.slice(0, -7); // trim off ?search
const hasQuery = searchQuery.length === 0;
this._resetOnNextBlur = hasQuery;
this._isSearchDispatchPending = false;
if (searchQuery) {
this.props.onSearch(searchQuery.trim());
}
}, WunderBar.TYPING_TIMEOUT); // 800ms delay, tweak for faster/slower
} }
componentWillReceiveProps(nextProps) { if (this.input) {
if ( this.input.blur();
nextProps.viewingPage !== this.props.viewingPage ||
nextProps.address != this.props.address
) {
this.setState({ address: nextProps.address, icon: nextProps.icon });
} }
}
onFocus() {
this._stateBeforeSearch = this.state;
const newState = {
icon: 'icon-search',
isActive: true,
};
this._focusPending = true;
// below is hacking, improved when we have proper routing
if (!this.state.address.startsWith('lbry://') && this.state.icon !== 'icon-search') {
// onFocus, if they are not on an exact URL or a search page, clear the bar
newState.address = '';
}
this.setState(newState);
}
onBlur() {
if (this._isSearchDispatchPending) {
setTimeout(() => {
this.onBlur();
}, WunderBar.TYPING_TIMEOUT + 1);
} else {
const commonState = { isActive: false };
if (this._resetOnNextBlur) {
this.setState(Object.assign({}, this._stateBeforeSearch, commonState));
this._input.value = this.state.address;
} else {
this._resetOnNextBlur = true;
this._stateBeforeSearch = this.state;
this.setState(commonState);
}
}
}
componentDidUpdate() {
if (this._input) {
const start = this._input.selectionStart,
end = this._input.selectionEnd;
this._input.value = this.state.address; // this causes cursor to go to end of input
this._input.setSelectionRange(start, end);
if (this._focusPending) {
this._input.select();
this._focusPending = false;
}
}
}
onKeyPress(event) {
if (event.charCode == 13 && this._input.value) {
let uri = null,
method = 'onSubmit',
extraParams = {};
this._resetOnNextBlur = false;
clearTimeout(this._userTypingTimer);
const parts = this._input.value.trim().split('?');
const value = parts.shift();
if (parts.length > 0) extraParams = parseQueryParams(parts.join(''));
try { try {
uri = lbryuri.normalize(value); const uri = lbryuri.normalize(value);
this.setState({ value: uri }); onSubmit(uri);
} catch (error) { } catch (e) {
// then it's not a valid URL, so let's search // search query isn't a valid uri
uri = value; onSearch(searchValue);
method = 'onSearch';
} }
this.props[method](uri, extraParams);
this._input.blur();
}
}
onReceiveRef(ref) {
this._input = ref;
} }
render() { render() {
const { searchQuery, isActive, address, suggestions } = this.props;
// if we are on the file/channel page
// use the address in the history stack
const wunderbarValue = isActive ? searchQuery : searchQuery || address;
return ( return (
<div className={`wunderbar${this.state.isActive ? ' wunderbar--active' : ''}`}> <div
{this.state.icon ? <Icon fixed icon={this.state.icon} /> : ''} className={classnames('header__wunderbar', {
'header__wunderbar--active': isActive,
})}
>
<Autocomplete
autoHighlight
ref={ref => {
this.input = ref;
}}
wrapperStyle={{ flex: 1, minHeight: 0 }}
value={wunderbarValue}
items={suggestions}
getItemValue={item => item.value}
onChange={this.handleChange}
onSelect={this.handleSubmit}
renderInput={props => (
<input <input
{...props}
className="wunderbar__input" className="wunderbar__input"
type="search" placeholder="Search for videos, music, games and more"
ref={this.onReceiveRef} />
onFocus={this.onFocus} )}
onBlur={this.onBlur} renderItem={(item, isHighlighted) => (
onChange={this.onChange} <div
onKeyPress={this.onKeyPress} key={item.value}
value={this.state.address} className={classnames('wunderbar__suggestion', {
placeholder={__('Find videos, music, games, and more')} 'wunderbar__active-suggestion': isHighlighted,
})}
>
{item.label}
</div>
)}
/> />
</div> </div>
); );

View file

@ -91,6 +91,10 @@ export const FILE_DELETE = 'FILE_DELETE';
export const SEARCH_STARTED = 'SEARCH_STARTED'; export const SEARCH_STARTED = 'SEARCH_STARTED';
export const SEARCH_COMPLETED = 'SEARCH_COMPLETED'; export const SEARCH_COMPLETED = 'SEARCH_COMPLETED';
export const SEARCH_CANCELLED = 'SEARCH_CANCELLED'; export const SEARCH_CANCELLED = 'SEARCH_CANCELLED';
export const UPDATE_SEARCH_QUERY = 'UPDATE_SEARCH_QUERY';
export const GET_SEARCH_SUGGESTIONS_START = 'GET_SEARCH_SUGGESTIONS_START';
export const GET_SEARCH_SUGGESTIONS_SUCCESS = 'GET_SEARCH_SUGGESTIONS_SUCCESS';
export const GET_SEARCH_SUGGESTIONS_FAIL = 'GET_SEARCH_SUGGESTIONS_FAIL';
// Settings // Settings
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED'; export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';

View file

@ -31,7 +31,7 @@ ipcRenderer.on('open-uri-requested', (event, uri, newSession) => {
try { try {
verification = JSON.parse(atob(uri.substring(15))); verification = JSON.parse(atob(uri.substring(15)));
} catch (error) { } catch (error) {
console.log(error); console.log(error); // eslint-disable-line no-console
} }
if (verification.token && verification.recaptcha) { if (verification.token && verification.recaptcha) {
app.store.dispatch(doConditionalAuthNavigate(newSession)); app.store.dispatch(doConditionalAuthNavigate(newSession));
@ -112,10 +112,10 @@ const init = () => {
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<div> <React.Fragment>
<App /> <App />
<SnackBar /> <SnackBar />
</div> </React.Fragment>
</Provider>, </Provider>,
document.getElementById('app') document.getElementById('app')
); );

View file

@ -1,3 +1,4 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import lbryuri from 'lbryuri'; import lbryuri from 'lbryuri';
import { BusyMessage } from 'component/common'; import { BusyMessage } from 'component/common';
@ -5,6 +6,7 @@ import FileTile from 'component/fileTile';
import ReactPaginate from 'react-paginate'; import ReactPaginate from 'react-paginate';
import Link from 'component/link'; import Link from 'component/link';
import SubscribeButton from 'component/subscribeButton'; import SubscribeButton from 'component/subscribeButton';
import Page from 'component/page';
class ChannelPage extends React.PureComponent { class ChannelPage extends React.PureComponent {
componentDidMount() { componentDidMount() {
@ -70,7 +72,7 @@ class ChannelPage extends React.PureComponent {
} }
return ( return (
<div> <Page>
<section className="card"> <section className="card">
<div className="card__inner"> <div className="card__inner">
<div className="card__title-identity"> <div className="card__title-identity">
@ -107,9 +109,10 @@ class ChannelPage extends React.PureComponent {
containerClassName="pagination" containerClassName="pagination"
/> />
)} )}
</div> </Page>
); );
} }
} }
export default ChannelPage; export default ChannelPage;
/* eslint-enable */

View file

@ -1,259 +1,37 @@
// @flow
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import Page from 'component/page';
import lbryuri from 'lbryuri'; import CategoryList from 'component/common/category-list';
import FileCard from 'component/fileCard';
import { BusyMessage } from 'component/common.js';
import Icon from 'component/icon';
import ToolTip from 'component/tooltip.js';
import SubHeader from 'component/subHeader';
import classnames from 'classnames';
import Link from 'component/link';
// This should be in a separate file type Props = {
export class FeaturedCategory extends React.PureComponent { fetchFeaturedUris: () => void,
constructor() { fetchingFeaturedUris: boolean,
super(); featuredUris: {},
};
this.state = { class DiscoverPage extends React.PureComponent<Props> {
numItems: undefined,
canScrollPrevious: false,
canScrollNext: false,
};
}
componentWillMount() {
this.setState({
numItems: this.props.names.length,
});
}
componentDidMount() {
const cardRow = ReactDOM.findDOMNode(this.refs.rowitems);
const cards = cardRow.getElementsByTagName('section');
// check if the last card is visible
const lastCard = cards[cards.length - 1];
const isCompletelyVisible = this.isCardVisible(lastCard, cardRow, false);
if (!isCompletelyVisible) {
this.setState({
canScrollNext: true,
});
}
}
handleScrollPrevious() {
const cardRow = ReactDOM.findDOMNode(this.refs.rowitems);
if (cardRow.scrollLeft > 0) {
// check the visible cards
const cards = cardRow.getElementsByTagName('section');
let firstVisibleCard = null;
let firstVisibleIdx = -1;
for (let i = 0; i < cards.length; i++) {
if (this.isCardVisible(cards[i], cardRow, false)) {
firstVisibleCard = cards[i];
firstVisibleIdx = i;
break;
}
}
const numDisplayed = this.numDisplayedCards(cardRow);
const scrollToIdx = firstVisibleIdx - numDisplayed;
const animationCallback = () => {
this.setState({
canScrollPrevious: cardRow.scrollLeft !== 0,
canScrollNext: true,
});
};
this.scrollCardItemsLeftAnimated(
cardRow,
scrollToIdx < 0 ? 0 : cards[scrollToIdx].offsetLeft,
100,
animationCallback
);
}
}
handleScrollNext() {
const cardRow = ReactDOM.findDOMNode(this.refs.rowitems);
// check the visible cards
const cards = cardRow.getElementsByTagName('section');
let lastVisibleCard = null;
let lastVisibleIdx = -1;
for (let i = 0; i < cards.length; i++) {
if (this.isCardVisible(cards[i], cardRow, true)) {
lastVisibleCard = cards[i];
lastVisibleIdx = i;
}
}
if (lastVisibleCard) {
const numDisplayed = this.numDisplayedCards(cardRow);
const animationCallback = () => {
// update last visible index after scroll
for (let i = 0; i < cards.length; i++) {
if (this.isCardVisible(cards[i], cardRow, true)) {
lastVisibleIdx = i;
}
}
this.setState({ canScrollPrevious: true });
if (lastVisibleIdx === cards.length - 1) {
this.setState({ canScrollNext: false });
}
};
this.scrollCardItemsLeftAnimated(
cardRow,
Math.min(lastVisibleCard.offsetLeft, cardRow.scrollWidth - cardRow.clientWidth),
100,
animationCallback
);
}
}
scrollCardItemsLeftAnimated(cardRow, target, duration, callback) {
if (!duration || duration <= diff) {
cardRow.scrollLeft = target;
if (callback) {
callback();
}
return;
}
const component = this;
const diff = target - cardRow.scrollLeft;
const tick = diff / duration * 10;
setTimeout(() => {
cardRow.scrollLeft += tick;
if (cardRow.scrollLeft === target) {
if (callback) {
callback();
}
return;
}
component.scrollCardItemsLeftAnimated(cardRow, target, duration - 10, callback);
}, 10);
}
isCardVisible(section, cardRow, partialVisibility) {
// check if a card is fully or partialy visible in its parent
const cardRowWidth = cardRow.offsetWidth;
const cardRowLeft = cardRow.scrollLeft;
const cardRowEnd = cardRowLeft + cardRow.offsetWidth;
const sectionLeft = section.offsetLeft - cardRowLeft;
const sectionEnd = sectionLeft + section.offsetWidth;
return (
(sectionLeft >= 0 && sectionEnd <= cardRowWidth) ||
(((sectionLeft < 0 && sectionEnd > 0) || (sectionLeft > 0 && sectionLeft <= cardRowWidth)) &&
partialVisibility)
);
}
numDisplayedCards(cardRow) {
const cards = cardRow.getElementsByTagName('section');
const cardRowWidth = cardRow.offsetWidth;
// get the width of the first card and then calculate
const cardWidth = cards.length > 0 ? cards[0].offsetWidth : 0;
if (cardWidth > 0) {
return Math.ceil(cardRowWidth / cardWidth);
}
// return a default value of 1 card displayed if the card width couldn't be determined
return 1;
}
render() {
const { category, names, categoryLink } = this.props;
return (
<div className="card-row card-row--small">
<h3 className="card-row__header">
{categoryLink ? (
<Link
className="button-text no-underline"
label={category}
navigate="/show"
navigateParams={{ uri: categoryLink }}
/>
) : (
category
)}
{category &&
category.match(/^community/i) && (
<ToolTip
label={__("What's this?")}
body={__(
'Community Content is a public space where anyone can share content with the rest of the LBRY community. Bid on the names "one," "two," "three," "four" and "five" to put your content here!'
)}
className="tooltip--header"
/>
)}
</h3>
<div className="card-row__scrollhouse">
{this.state.canScrollPrevious && (
<div className="card-row__nav card-row__nav--left">
<a className="card-row__scroll-button" onClick={this.handleScrollPrevious.bind(this)}>
<Icon icon="icon-chevron-left" />
</a>
</div>
)}
{this.state.canScrollNext && (
<div className="card-row__nav card-row__nav--right">
<a className="card-row__scroll-button" onClick={this.handleScrollNext.bind(this)}>
<Icon icon="icon-chevron-right" />
</a>
</div>
)}
<div ref="rowitems" className="card-row__items">
{names &&
names.map(name => (
<FileCard key={name} displayStyle="card" uri={lbryuri.normalize(name)} />
))}
</div>
</div>
</div>
);
}
}
class DiscoverPage extends React.PureComponent {
componentWillMount() { componentWillMount() {
this.props.fetchFeaturedUris(); this.props.fetchFeaturedUris();
} }
render() { render() {
const { featuredUris, fetchingFeaturedUris } = this.props; const { featuredUris, fetchingFeaturedUris } = this.props;
const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length, const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length;
failedToLoad = !fetchingFeaturedUris && !hasContent; const failedToLoad = !fetchingFeaturedUris && !hasContent;
return ( return (
<main <Page noPadding isLoading={!hasContent && fetchingFeaturedUris}>
className={classnames('main main--no-margin', {
reloading: hasContent && fetchingFeaturedUris,
})}
>
<SubHeader fullWidth smallMargin />
{!hasContent && fetchingFeaturedUris && <BusyMessage message={__('Fetching content')} />}
{hasContent && {hasContent &&
Object.keys(featuredUris).map( Object.keys(featuredUris).map(
category => category =>
featuredUris[category].length ? ( featuredUris[category].length ? (
<FeaturedCategory <CategoryList key={category} category={category} names={featuredUris[category]} />
key={category}
category={category}
names={featuredUris[category]}
/>
) : ( ) : (
'' ''
) )
)} )}
{failedToLoad && <div className="empty">{__('Failed to load landing content.')}</div>} {failedToLoad && <div className="empty">{__('Failed to load landing content.')}</div>}
</main> </Page>
); );
} }
} }

View file

@ -1,3 +1,4 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import lbry from 'lbry'; import lbry from 'lbry';
import lbryuri from 'lbryuri'; import lbryuri from 'lbryuri';
@ -6,12 +7,13 @@ import { Thumbnail } from 'component/common';
import FilePrice from 'component/filePrice'; import FilePrice from 'component/filePrice';
import FileDetails from 'component/fileDetails'; import FileDetails from 'component/fileDetails';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import Icon from 'component/icon'; import Icon from 'component/common/icon';
import WalletSendTip from 'component/walletSendTip'; import WalletSendTip from 'component/walletSendTip';
import DateTime from 'component/dateTime'; import DateTime from 'component/dateTime';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import Link from 'component/link'; import Link from 'component/link';
import SubscribeButton from 'component/subscribeButton'; import SubscribeButton from 'component/subscribeButton';
import Page from 'component/page';
class FilePage extends React.PureComponent { class FilePage extends React.PureComponent {
componentDidMount() { componentDidMount() {
@ -69,6 +71,7 @@ class FilePage extends React.PureComponent {
} }
return ( return (
<Page>
<section className={`card ${obscureNsfw ? 'card--obscured ' : ''}`}> <section className={`card ${obscureNsfw ? 'card--obscured ' : ''}`}>
<div className="show-page-media"> <div className="show-page-media">
{isPlayable ? ( {isPlayable ? (
@ -110,8 +113,10 @@ class FilePage extends React.PureComponent {
{tab === 'tip' && <WalletSendTip claim_id={claim.claim_id} uri={uri} />} {tab === 'tip' && <WalletSendTip claim_id={claim.claim_id} uri={uri} />}
</div> </div>
</section> </section>
</Page>
); );
} }
} }
export default FilePage; export default FilePage;
/* eslint-enable */

View file

@ -1,15 +1,21 @@
// @flow
import React from 'react'; import React from 'react';
import lbryuri from 'lbryuri'; import lbryuri from 'lbryuri';
import FileTile from 'component/fileTile'; import FileTile from 'component/fileTile';
import FileListSearch from 'component/fileListSearch'; import FileListSearch from 'component/fileListSearch';
import { ToolTip } from 'component/tooltip.js'; import ToolTip from 'component/common/tooltip';
import Page from 'component/page';
class SearchPage extends React.PureComponent { type Props = {
query: ?string,
};
class SearchPage extends React.PureComponent<Props> {
render() { render() {
const { query } = this.props; const { query } = this.props;
return ( return (
<main className="main--single-column"> <Page>
{lbryuri.isValid(query) ? ( {lbryuri.isValid(query) ? (
<section className="section-spaced"> <section className="section-spaced">
<h3 className="card-row__header"> <h3 className="card-row__header">
@ -36,7 +42,7 @@ class SearchPage extends React.PureComponent {
</h3> </h3>
<FileListSearch query={query} /> <FileListSearch query={query} />
</section> </section>
</main> </Page>
); );
} }
} }

View file

@ -1,17 +1,24 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import lbryuri from 'lbryuri';
import { BusyMessage } from 'component/common'; import { BusyMessage } from 'component/common';
import ChannelPage from 'page/channel'; import ChannelPage from 'page/channel';
import FilePage from 'page/file'; import FilePage from 'page/file';
class ShowPage extends React.PureComponent { type Props = {
isResolvingUri: boolean,
resolveUri: string => void,
uri: string,
claim: { name: string },
};
class ShowPage extends React.PureComponent<Props> {
componentWillMount() { componentWillMount() {
const { isResolvingUri, resolveUri, uri } = this.props; const { isResolvingUri, resolveUri, uri } = this.props;
if (!isResolvingUri) resolveUri(uri); if (!isResolvingUri) resolveUri(uri);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps: Props) {
const { isResolvingUri, resolveUri, claim, uri } = nextProps; const { isResolvingUri, resolveUri, claim, uri } = nextProps;
if (!isResolvingUri && claim === undefined && uri) { if (!isResolvingUri && claim === undefined && uri) {
@ -47,8 +54,9 @@ class ShowPage extends React.PureComponent {
innerContent = <FilePage uri={uri} />; innerContent = <FilePage uri={uri} />;
} }
return <main className="main--single-column">{innerContent}</main>; return innerContent;
} }
} }
export default ShowPage; export default ShowPage;
/* eslint-enable */

View file

@ -1,8 +1,8 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import SubHeader from 'component/subHeader'; import SubHeader from 'component/subHeader';
import { BusyMessage } from 'component/common.js'; import { BusyMessage } from 'component/common';
import { FeaturedCategory } from 'page/discover/view'; import CategoryList from 'component/common/category-list';
import type { Subscription } from 'redux/reducers/subscriptions'; import type { Subscription } from 'redux/reducers/subscriptions';
type SavedSubscriptions = Array<Subscription>; type SavedSubscriptions = Array<Subscription>;
@ -83,7 +83,7 @@ export default class extends React.PureComponent<Props> {
} }
return ( return (
<FeaturedCategory <CategoryList
key={subscription.channelName} key={subscription.channelName}
categoryLink={subscription.uri} categoryLink={subscription.uri}
category={subscription.channelName} category={subscription.channelName}

View file

@ -5,9 +5,14 @@ import { doNavigate } from 'redux/actions/navigation';
import { selectCurrentPage } from 'redux/selectors/navigation'; import { selectCurrentPage } from 'redux/selectors/navigation';
import batchActions from 'util/batchActions'; import batchActions from 'util/batchActions';
// TODO: this should be in a util
const handleResponse = response =>
response.status === 200
? Promise.resolve(response.json())
: Promise.reject(new Error(response.statusText));
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export function doSearch(rawQuery) { export const doSearch = rawQuery => (dispatch, getState) => {
return (dispatch, getState) => {
const state = getState(); const state = getState();
const page = selectCurrentPage(state); const page = selectCurrentPage(state);
@ -29,12 +34,7 @@ export function doSearch(rawQuery) {
dispatch(doNavigate('search', { query })); dispatch(doNavigate('search', { query }));
} else { } else {
fetch(`https://lighthouse.lbry.io/search?s=${query}`) fetch(`https://lighthouse.lbry.io/search?s=${query}`)
.then( .then(handleResponse)
response =>
response.status === 200
? Promise.resolve(response.json())
: Promise.reject(new Error(response.statusText))
)
.then(data => { .then(data => {
const uris = []; const uris = [];
const actions = []; const actions = [];
@ -63,5 +63,62 @@ export function doSearch(rawQuery) {
}); });
}); });
} }
}; };
}
export const updateSearchQuery = searchQuery => ({
type: ACTIONS.UPDATE_SEARCH_QUERY,
data: { searchQuery },
});
export const getSearchSuggestions = value => dispatch => {
dispatch({ type: ACTIONS.GET_SEARCH_SUGGESTIONS_START });
if (!value) {
dispatch({
type: ACTIONS.GET_SEARCH_SUGGESTIONS_SUCCESS,
data: [],
});
return;
}
// This should probably be more robust
let searchValue = value;
if (searchValue.startsWith('lbry://')) {
searchValue = searchValue.slice(7);
}
// need to handle spaces in the query?
fetch(`https://lighthouse.lbry.io/autocomplete?s=${searchValue}`)
.then(handleResponse)
.then(suggestions => {
const formattedSuggestions = suggestions.slice(0, 5).map(suggestion => ({
label: suggestion,
value: suggestion,
}));
// Should we add lbry://{query} as the first result?
// If it's not a valid uri, then add a "search for {query}" result
const searchLabel = `Search for "${value}"`;
try {
const uri = Lbryuri.normalize(value);
formattedSuggestions.unshift(
{ label: uri, value: uri },
{ label: searchLabel, value: `${value}?search` }
);
} catch (e) {
if (value) {
formattedSuggestions.unshift({ label: searchLabel, value });
}
}
return dispatch({
type: ACTIONS.GET_SEARCH_SUGGESTIONS_SUCCESS,
data: formattedSuggestions,
});
})
.catch(err =>
dispatch({
type: ACTIONS.GET_SEARCH_SUGGESTIONS_FAIL,
data: err,
})
);
};

View file

@ -1,32 +1,75 @@
// @flow
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import { handleActions } from 'util/redux-utils';
const reducers = {}; type SearchState = {
const defaultState = { isActive: boolean,
searchQuery: string,
searchingForSuggestions: boolean,
suggestions: Array<string>,
urisByQuery: {}, urisByQuery: {},
searching: false,
}; };
reducers[ACTIONS.SEARCH_STARTED] = state => const defaultState = {
Object.assign({}, state, { isActive: false,
searching: true, searchQuery: '', // needs to be an empty string for input focusing
}); searchingForSuggestions: false,
suggestions: [],
urisByQuery: {},
};
reducers[ACTIONS.SEARCH_COMPLETED] = (state, action) => { export default handleActions(
{
[ACTIONS.SEARCH_STARTED]: (state: SearchState): SearchState => ({
...state,
searching: true,
}),
[ACTIONS.SEARCH_COMPLETED]: (state: SearchState, action): SearchState => {
const { query, uris } = action.data; const { query, uris } = action.data;
return Object.assign({}, state, { return {
...state,
searching: false, searching: false,
urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }), urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }),
}); };
}; },
reducers[ACTIONS.SEARCH_CANCELLED] = state => [ACTIONS.SEARCH_CANCELLED]: (state: SearchState): SearchState => ({
Object.assign({}, state, { ...state,
searching: false, searching: false,
}); }),
export default function reducer(state = defaultState, action) { [ACTIONS.UPDATE_SEARCH_QUERY]: (state: SearchState, action): SearchState => ({
const handler = reducers[action.type]; ...state,
if (handler) return handler(state, action); searchQuery: action.data.searchQuery,
return state; suggestions: [],
} isActive: true,
}),
[ACTIONS.GET_SEARCH_SUGGESTIONS_START]: (state: SearchState): SearchState => ({
...state,
searchingForSuggestions: true,
suggestions: [],
}),
[ACTIONS.GET_SEARCH_SUGGESTIONS_SUCCESS]: (state: SearchState, action): SearchState => ({
...state,
searchingForSuggestions: false,
suggestions: action.data,
}),
[ACTIONS.GET_SEARCH_SUGGESTIONS_FAIL]: (state: SearchState): SearchState => ({
...state,
searchingForSuggestions: false,
// error, TODO: figure this out on the search page
}),
// clear the searchQuery on back/forward
// it may be populated by the page title for search/file pages
// if going home, it should be blank
[ACTIONS.HISTORY_NAVIGATE]: (state: SearchState): SearchState => ({
...state,
searchQuery: '',
isActive: false,
}),
},
defaultState
);

View file

@ -68,30 +68,6 @@ export const selectPageTitle = createSelector(
selectCurrentParams, selectCurrentParams,
(page, params) => { (page, params) => {
switch (page) { switch (page) {
case 'settings':
return __('Settings');
case 'report':
return __('Report');
case 'wallet':
return __('Wallet');
case 'send':
return __('Send or Receive LBRY Credits');
case 'getcredits':
return __('Get LBRY Credits');
case 'backup':
return __('Backup Your Wallet');
case 'rewards':
return __('Rewards');
case 'invite':
return __('Invites');
case 'start':
return __('Start');
case 'publish':
return params.id ? __('Edit') : __('Publish');
case 'help':
return __('Help');
case 'developer':
return __('Developer');
case 'show': { case 'show': {
const parts = [Lbryuri.normalize(params.uri)]; const parts = [Lbryuri.normalize(params.uri)];
// If the params has any keys other than "uri" // If the params has any keys other than "uri"
@ -100,21 +76,14 @@ export const selectPageTitle = createSelector(
} }
return parts.join('?'); return parts.join('?');
} }
case 'downloaded':
return __('Downloads & Purchases');
case 'published':
return __('Publications');
case 'search':
return params.query ? __('Search results for %s', params.query) : __('Search');
case 'subscriptions':
return __('Your Subscriptions');
case 'discover': case 'discover':
return __('Discover');
case false: case false:
case null: case null:
case '': case '':
return ''; return '';
default: default:
return page[0].toUpperCase() + (page.length > 0 ? page.substr(1) : ''); return '';
} }
} }
); );

View file

@ -28,52 +28,15 @@ export const selectWunderBarAddress = createSelector(
selectCurrentPage, selectCurrentPage,
selectPageTitle, selectPageTitle,
selectSearchQuery, selectSearchQuery,
(page, title, query) => (page !== 'search' ? title : query || title) (page, title, query) => {
); // only populate the wunderbar address if we are on the file/channel pages
// or show the search query
if (page === 'show') {
return title;
} else if (page === 'search') {
return query;
}
export const selectWunderBarIcon = createSelector( return '';
selectCurrentPage,
selectCurrentParams,
(page, params) => {
switch (page) {
case 'auth':
return 'icon-user';
case 'settings':
return 'icon-gear';
case 'help':
return 'icon-question';
case 'report':
return 'icon-file';
case 'downloaded':
return 'icon-folder';
case 'published':
return 'icon-folder';
case 'history':
return 'icon-history';
case 'send':
return 'icon-send';
case 'rewards':
return 'icon-rocket';
case 'invite':
return 'icon-envelope-open';
case 'getcredits':
return 'icon-shopping-cart';
case 'wallet':
case 'backup':
return 'icon-bank';
case 'show':
return 'icon-file';
case 'publish':
return params.id ? __('icon-pencil') : __('icon-upload');
case 'developer':
return 'icon-code';
case 'discover':
case 'search':
return 'icon-search';
case 'subscriptions':
return 'icon-th-list';
default:
return 'icon-file';
}
} }
); );

View file

@ -1,4 +1,54 @@
@import url(https://fonts.googleapis.com/css?family=Roboto:400,400i,500,500i,700); // Generic html styles used accross the App
// component specific styling should go in the component scss file
@font-face {
font-family: 'Metropolis';
font-weight: normal;
font-style: normal;
text-rendering: optimizeLegibility;
src: url('../../../static/font/metropolis/Metropolis-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'Metropolis';
font-weight: 600;
font-style: normal;
text-rendering: optimizeLegibility;
src: url('../../../static/font/metropolis/Metropolis-SemiBold.woff2') format('woff2');
}
// @font-face {
// font-family: 'Metropolis';
// font-weight: 700;
// font-style: normal;
// text-rendering: optimizeLegibility;
// src: url('../../../static/font/metropolis/Metropolis-SemiBold.woff2') format('woff2');
// }
@font-face {
font-family: 'Metropolis';
font-weight: 800;
font-style: normal;
text-rendering: optimizeLegibility;
src: url('../../../static/font/metropolis/Metropolis-ExtraBold.woff2') format('woff2');
}
// TODO: use this
// @font-face {
// font-family: 'Metropolis';
// font-weight: normal;
// font-style: italic;
// text-rendering: optimizeLegibility;
// src: url('../../../static/font/metropolis/Metropolis-MediumItalic.woff2') format('woff2');
// }
//
// @font-face {
// font-family: 'Metropolis';
// font-weight: lighter;
// font-style: normal;
// text-rendering: optimizeLegibility;
// src: url('../../../static/font/metropolis/Metropolis-Light.woff2') format('woff2');
// }
html { html {
height: 100%; height: 100%;
@ -7,84 +57,20 @@ html {
body { body {
color: var(--text-color); color: var(--text-color);
font-family: 'Roboto', sans-serif; font-family: 'Metropolis', sans-serif;
line-height: var(--font-line-height); line-height: var(--font-line-height);
height: 100%;
overflow: hidden;
} }
/* Custom text selection */ h1,
*::selection { h2,
background: var(--text-selection-bg); h3,
color: var(--text-selection-color); h4,
} h5 {
#window {
min-height: 100vh;
background: var(--window-bg);
}
.credit-amount--indicator {
font-weight: 500;
color: var(--color-money);
}
.credit-amount--fee {
font-size: 0.9em;
color: var(--color-meta-light);
}
.credit-amount--bold {
font-weight: 700; font-weight: 700;
} }
#main-content {
margin: auto;
display: flex;
flex-direction: column;
overflow: overlay;
padding: $spacing-vertical;
position: absolute;
top: var(--header-height);
bottom: 0;
left: 4px;
right: 4px;
main {
margin-left: auto;
margin-right: auto;
max-width: 100%;
}
main.main--single-column {
width: $width-page-constrained;
}
main.main--no-margin {
margin-left: 0;
margin-right: 0;
}
}
.reloading {
&:before {
$width: 30px;
position: absolute;
background: url('../../../static/img/busy.gif') no-repeat center center;
width: $width;
height: $spacing-vertical;
content: '';
left: 50%;
margin-left: -1 / 2 * $width;
display: inline-block;
}
}
.icon-fixed-width {
/* This borrowed is from a component of Font Awesome we're not using, maybe add it? */
width: (18em / 14);
text-align: center;
}
.icon--left-pad {
padding-left: 3px;
}
h2 { h2 {
font-size: 1.75em; font-size: 1.75em;
} }
@ -100,11 +86,13 @@ h4 {
h5 { h5 {
font-size: 1.1em; font-size: 1.1em;
} }
sup, sup,
sub { sub {
vertical-align: baseline; vertical-align: baseline;
position: relative; position: relative;
} }
sup { sup {
top: -0.4em; top: -0.4em;
} }
@ -117,11 +105,66 @@ code {
background-color: var(--color-bg-alt); background-color: var(--color-bg-alt);
} }
p { // Why is this needed?
margin-bottom: 0.8em; button {
&:last-child { font-family: inherit;
margin-bottom: 0; }
}
#window {
height: 100%;
overflow: hidden; // so the scrollbar will not extend into the header
}
#main-content {
height: 100%;
overflow-y: auto;
position: absolute;
left: 0px;
right: 0px;
// don't use {bottom/top} here
// they cause flashes of un-rendered content when scrolling
margin-top: var(--header-height);
padding-bottom: var(--header-height); // fix this scrollbar extends beyond screen
background-color: var(--color-bg);
}
.main {
padding: 0 $spacing-vertical * 2/3;
}
.main--no-padding {
padding-left: 0;
padding-right: 0;
}
.page__header {
padding: $spacing-vertical * 2/3;
padding-bottom: 0;
}
.page__title {
font-weight: 800;
font-size: 3em;
}
/* Custom text selection */
*::selection {
background: var(--text-selection-bg);
color: var(--text-selection-color);
}
.credit-amount--indicator {
font-weight: 500;
color: var(--color-money);
}
.credit-amount--fee {
font-size: 0.9em;
color: var(--color-meta-light);
}
.credit-amount--bold {
font-weight: 700;
} }
.hidden { .hidden {
@ -192,7 +235,3 @@ p {
section.section-spaced { section.section-spaced {
margin-bottom: $spacing-vertical; margin-bottom: $spacing-vertical;
} }
.text-center {
text-align: center;
}

View file

@ -27,6 +27,16 @@
transform: translate(0, 0); transform: translate(0, 0);
} }
.icon--fixed-width {
/* This borrowed is from a component of Font Awesome we're not using, maybe add it? */
width: (18em / 14);
text-align: center;
}
.icon--padded {
padding: 0 3px;
}
/* Adjustments for icon size and alignment */ /* Adjustments for icon size and alignment */
.icon-rocket { .icon-rocket {
color: orangered; color: orangered;

View file

@ -6,6 +6,8 @@ $width-page-constrained: 800px;
$text-color: #000; $text-color: #000;
:root { :root {
--spacing-vertical: 24px;
/* Colors */ /* Colors */
--color-brand: #155b4a; --color-brand: #155b4a;
--color-primary: #155b4a; --color-primary: #155b4a;
@ -21,7 +23,8 @@ $text-color: #000;
--color-download: rgba(0, 0, 0, 0.75); --color-download: rgba(0, 0, 0, 0.75);
--color-canvas: #f5f5f5; --color-canvas: #f5f5f5;
--color-bg: #ffffff; --color-bg: #ffffff;
--color-bg-alt: #d9d9d9; --color-bg-alt: #f6f6f6;
--color-placeholder: #ececec;
/* Misc */ /* Misc */
--content-max-width: 1000px; --content-max-width: 1000px;
@ -34,7 +37,7 @@ $text-color: #000;
--font-size-subtext-multiple: 0.82; --font-size-subtext-multiple: 0.82;
/* Shadows */ /* Shadows */
--box-shadow-layer: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); --box-shadow-layer: 0px 1px 3px 0px rgba(0, 0, 0, 0.2);
--box-shadow-focus: 2px 4px 4px 0 rgba(0, 0, 0, 0.14), 2px 5px 3px -2px rgba(0, 0, 0, 0.2), --box-shadow-focus: 2px 4px 4px 0 rgba(0, 0, 0, 0.14), 2px 5px 3px -2px rgba(0, 0, 0, 0.2),
2px 3px 7px 0 rgba(0, 0, 0, 0.12); 2px 3px 7px 0 rgba(0, 0, 0, 0.12);
@ -50,9 +53,6 @@ $text-color: #000;
--text-selection-bg: rgba(saturate(lighten(#155b4a, 20%), 20%), 1); // temp color --text-selection-bg: rgba(saturate(lighten(#155b4a, 20%), 20%), 1); // temp color
--text-selection-color: #fff; --text-selection-color: #fff;
/* Window */
--window-bg: var(--color-canvas);
/* Form */ /* Form */
--form-label-color: rgba(0, 0, 0, 0.54); --form-label-color: rgba(0, 0, 0, 0.54);
@ -80,21 +80,23 @@ $text-color: #000;
--select-bg: var(--color-bg-alt); --select-bg: var(--color-bg-alt);
--select-color: var(--text-color); --select-color: var(--text-color);
//TODO: determine proper button variables;
/* Button */ /* Button */
--button-bg: var(--color-bg-alt); --btn-primary-color: #fff;
--button-color: #fff; --button-alt-color: var(--text-color);
--button-primary-bg: var(--color-primary); --btn-primary-bg: var(--color-primary);
--button-primary-color: #fff; --btn-alt-bg: red;
--button-padding: $spacing-vertical * 2/3; --btn-radius: 10px;
--button-height: $spacing-vertical * 1.5; // below needed?
--button-intra-margin: $spacing-vertical; --btn-padding: $spacing-vertical * 2/3;
--button-radius: 3px; --btn-height: $spacing-vertical * 1.5;
--btn-intra-margin: $spacing-vertical;
/* Header */ /* Header */
--header-bg: var(--color-bg); --header-bg: var(--color-bg);
--header-color: #666; --header-color: #666;
--header-active-color: rgba(0, 0, 0, 0.85); --header-active-color: rgba(0, 0, 0, 0.85);
--header-height: $spacing-vertical * 2.5; --header-height: 65px;
--header-button-bg: transparent; //var(--button-bg); --header-button-bg: transparent; //var(--button-bg);
--header-button-hover-bg: rgba(100, 100, 100, 0.15); --header-button-hover-bg: rgba(100, 100, 100, 0.15);
@ -142,7 +144,6 @@ $text-color: #000;
--tooltip-width: 300px; --tooltip-width: 300px;
--tooltip-bg: var(--color-bg); --tooltip-bg: var(--color-bg);
--tooltip-color: var(--text-color); --tooltip-color: var(--text-color);
--tooltip-border: 1px solid #aaa;
/* Scrollbar */ /* Scrollbar */
--scrollbar-radius: 10px; --scrollbar-radius: 10px;

View file

@ -1,89 +1,100 @@
@import '../mixin/link.scss'; // Styles for the <Button /> component
// may be a button or a html element
$button-focus-shift: 12%; /*
.button-set-item { TODO:
position: relative; Determine [disabled] or .disabled
display: inline-block;
+ .button-set-item { */
margin-left: var(--button-intra-margin);
}
}
.button-block, button {
.faux-button-block { border: none;
display: inline-block;
height: var(--button-height);
line-height: var(--button-height);
text-decoration: none; text-decoration: none;
border: 0 none;
text-align: center;
border-radius: var(--button-radius);
text-transform: uppercase;
.icon {
top: 0em;
}
.icon:first-child {
padding-right: 5px;
}
.icon:last-child {
padding-left: 5px;
}
.icon:only-child {
padding-left: 0;
padding-right: 0;
}
}
.button-block {
cursor: pointer; cursor: pointer;
font-weight: 500; position: relative;
font-size: 14px;
user-select: none;
transition: background var(--animation-duration) var(--animation-style);
} }
.button__content { button:disabled.btn--disabled {
margin: 0 var(--button-padding); cursor: default;
display: flex; background-color: transparent;
.link-label {
text-decoration: none !important;
}
} }
.button-primary { button.btn {
color: var(--button-primary-color); padding: 10px;
background-color: var(--button-primary-bg); margin: 0 5px;
border-radius: var(--btn-radius);
color: var(--btn-primary-color);
background-color: var(--btn-primary-bg);
&:hover:not(.btn--disabled) {
box-shadow: var(--box-shadow-layer); box-shadow: var(--box-shadow-layer);
&:focus {
//color: var(--button-primary-active-color);
//background-color:color: var(--button-primary-active-bg);
//box-shadow: $box-shadow-focus;
} }
} }
.button-alt {
background-color: var(--button-bg); button.btn.btn--alt {
color: var(--btn-alt-color);
background-color: #efefef;
&:hover {
color: #111;
}
&:active {
background-color: #cdcdcd;
}
&:disabled {
color: var(--color-help);
background-color: transparent;
}
}
button.btn.btn--circle {
border-radius: 50%;
transition: all 0.2s;
&:hover:not([disabled]) {
border-radius: var(--btn-radius);
}
}
button.btn.btn--inverse {
box-shadow: none;
background-color: transparent;
color: var(--btn-primary-bg);
}
button.btn--link {
padding: 0;
margin: 0;
background-color: inherit;
font-size: 0.9em;
color: var(--btn-primary-bg); // this should be a different color
&:hover {
border-bottom: 1px solid;
}
}
.btn__label {
padding: 0 5px;
}
.btn--with-help + .button__help {
position: absolute;
// display: none;
transition: 0s display;
z-index: 1;
width: 120px;
// top: -5px;
left: 105%;
border-radius: 5px;
background-color: var(--color-placeholder);
box-shadow: var(--box-shadow-layer); box-shadow: var(--box-shadow-layer);
} }
.button-text { .btn--with-help:hover + .button__help {
@include text-link(); display: block;
display: inline-block; transition-delay: 1s;
.button__content {
margin: 0 var(--text-link-padding);
}
}
.button-text-help {
@include text-link(var(--text-help-color));
font-size: 0.8em;
}
.button--flat {
box-shadow: none !important;
}
.button--submit {
font-family: inherit;
line-height: 0;
} }

View file

@ -2,194 +2,19 @@
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
max-width: var(--card-max-width); max-width: var(--card-max-width);
background: var(--card-bg);
box-shadow: var(--box-shadow-layer);
border-radius: var(--card-radius); border-radius: var(--card-radius);
margin-bottom: var(--card-margin);
overflow: auto; overflow: auto;
user-select: text; user-select: text;
//below added to prevent scrollbar on long titles when show page loads, would prefer a cleaner CSS solution
overflow-x: hidden;
}
.card--obscured {
position: relative;
}
.card--obscured .card__inner {
filter: blur(var(--nsfw-blur-intensity));
}
.card__title-primary,
.card__title-identity,
.card__content,
.card__subtext,
.card__actions {
padding: 0 var(--card-padding);
}
.card--small {
.card__title-primary,
.card__title-identity,
.card__actions,
.card__content,
.card__subtext {
padding: 0 calc(var(--card-padding) / 2);
}
}
.card__title-primary {
margin-top: var(--card-margin);
margin-bottom: var(--card-margin);
}
.card__title-primary .meta {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card__title-identity {
margin: 16px 0;
}
.card__actions {
margin-top: var(--card-margin);
margin-bottom: var(--card-margin);
user-select: none;
}
.card__actions--bottom {
margin-top: $spacing-vertical * 1/3;
margin-bottom: $spacing-vertical * 1/3;
border-top: var(--divider);
}
.card__actions--form-submit {
margin-top: $spacing-vertical;
margin-bottom: var(--card-margin);
}
.card__action--right {
float: right;
}
.card__content {
margin-top: var(--card-margin);
margin-bottom: var(--card-margin);
table:not(:last-child) {
margin-bottom: var(--card-margin);
}
}
.card__actions--only-vertical {
margin-left: 0;
margin-right: 0;
padding-left: 0;
padding-right: 0;
}
.card__content--extra-vertical-space {
margin: $spacing-vertical 0;
}
$font-size-subtext-multiple: 0.82;
.card__subtext {
color: var(--color-meta-light);
font-size: calc(var(--font-size-subtext-multiple) * 1em);
margin-top: $spacing-vertical * 1/3;
margin-bottom: $spacing-vertical * 1/3;
}
.card__subtext--allow-newlines {
white-space: pre-wrap;
}
.card__subtext--two-lines {
height: calc(
var(--font-size) * var(--font-size-subtext-multiple) * var(--font-line-height) * 2
); /*this is so one line text still has the proper height*/
}
.card-overlay {
position: absolute;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
padding: 20px;
background-color: var(--color-dark-overlay);
color: #fff;
display: flex; display: flex;
align-items: center;
font-weight: 600;
} }
.card__link { .card--placeholder {
display: block; background-color: black;
cursor: pointer;
}
.card--link {
transition: transform 0.2s var(--animation-style);
}
.card--link:hover {
position: relative;
z-index: 1;
box-shadow: var(--box-shadow-focus);
transform: scale(var(--card-link-scaling)) translateX(var(--card-hover-translate));
transform-origin: 50% 50%;
overflow-x: visible;
overflow-y: visible;
}
.card--link:hover ~ .card--link {
transform: translateX(calc(var(--card-hover-translate) * 2));
}
.card__media {
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 50%;
}
.card__media--autothumb {
position: relative;
}
.card__media--autothumb.purple {
background-color: #9c27b0;
}
.card__media--autothumb.red {
background-color: #e53935;
}
.card__media--autothumb.pink {
background-color: #e91e63;
}
.card__media--autothumb.indigo {
background-color: #3f51b5;
}
.card__media--autothumb.blue {
background-color: #2196f3;
}
.card__media--autothumb.light-blue {
background-color: #039be5;
}
.card__media--autothumb.cyan {
background-color: #00acc1;
}
.card__media--autothumb.teal {
background-color: #009688;
}
.card__media--autothumb.green {
background-color: #43a047;
}
.card__media--autothumb.yellow {
background-color: #ffeb3b;
}
.card__media--autothumb.orange {
background-color: #ffa726;
}
.card__media--autothumb .card__autothumb__text {
font-size: 2em;
width: 100%;
color: #ffffff;
text-align: center;
position: absolute;
top: 36%;
}
.card__indicators {
float: right;
} }
.card--small { .card--small {
width: var(--card-small-width); width: var(--card-small-width);
min-height: var(--card-small-width);
overflow-x: hidden; overflow-x: hidden;
white-space: normal; white-space: normal;
} }
@ -197,126 +22,237 @@ $font-size-subtext-multiple: 0.82;
height: calc(var(--card-small-width) * 9 / 16); height: calc(var(--card-small-width) * 9 / 16);
} }
.card--form { .card__link {
width: calc(var(--input-width) + var(--card-padding) * 2); cursor: pointer;
// TODO: hover animations
// :hover {
//
// }
}
.card__media {
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 50%;
background-color: var(--color-placeholder);
}
.card__media--autothumb {
display: flex;
justify-content: center;
align-items: center;
}
.card__title-identity {
margin-top: $spacing-vertical * 1/3;
}
// TODO: regular .card__title for show page
.card__title--small {
font-weight: 600;
font-size: 0.9em;
} }
.card__subtitle { .card__subtitle {
color: var(--color-help); color: var(--color-help);
font-size: 0.85em; font-size: 0.85em;
line-height: calc(var(--font-line-height) * 1 / 0.85); padding-top: $spacing-vertical * 1/3;
} }
.card--file-subtitle { // .card__title-primary .meta {
display: flex; // white-space: nowrap;
} // overflow: hidden;
// text-overflow: ellipsis;
// }
//
// this is too specific //
// it should be a helper class // .card__actions {
// ex. ".m-padding-left" // margin-top: var(--card-margin);
// will come back to this during the redesign - sean // margin-bottom: var(--card-margin);
.card__publish-date { // user-select: none;
padding-left: 20px; // }
} //
// .card__actions--bottom {
// margin-top: $spacing-vertical * 1/3;
// margin-bottom: $spacing-vertical * 1/3;
// border-top: var(--divider);
// }
//
// .card__actions--form-submit {
// margin-top: $spacing-vertical;
// margin-bottom: var(--card-margin);
// }
//
// .card__action--right {
// float: right;
// }
//
// .card__content {
// margin-top: var(--card-margin);
// margin-bottom: var(--card-margin);
// table:not(:last-child) {
// margin-bottom: var(--card-margin);
// }
// }
//
// .card__actions--only-vertical {
// margin-left: 0;
// margin-right: 0;
// padding-left: 0;
// padding-right: 0;
// }
//
// .card__content--extra-vertical-space {
// margin: $spacing-vertical 0;
// }
//
// $font-size-subtext-multiple: 0.82;
// .card__subtext {
// color: var(--color-meta-light);
// font-size: calc(var(--font-size-subtext-multiple) * 1em);
// margin-top: $spacing-vertical * 1/3;
// margin-bottom: $spacing-vertical * 1/3;
// }
// .card__subtext--allow-newlines {
// white-space: pre-wrap;
// }
// .card__subtext--two-lines {
// height: calc(
// var(--font-size) * var(--font-size-subtext-multiple) * var(--font-line-height) * 2
// ); /*this is so one line text still has the proper height*/
// }
// .card-overlay {
// position: absolute;
// left: 0px;
// right: 0px;
// top: 0px;
// bottom: 0px;
// padding: 20px;
// background-color: var(--color-dark-overlay);
// color: #fff;
// display: flex;
// align-items: center;
// font-weight: 600;
// }
//
//
// .card__media--autothumb {
// position: relative;
// }
// .card__media--autothumb.purple {
// background-color: #9c27b0;
// }
// .card__media--autothumb.red {
// background-color: #e53935;
// }
// .card__media--autothumb.pink {
// background-color: #e91e63;
// }
// .card__media--autothumb.indigo {
// background-color: #3f51b5;
// }
// .card__media--autothumb.blue {
// background-color: #2196f3;
// }
// .card__media--autothumb.light-blue {
// background-color: #039be5;
// }
// .card__media--autothumb.cyan {
// background-color: #00acc1;
// }
// .card__media--autothumb.teal {
// background-color: #009688;
// }
// .card__media--autothumb.green {
// background-color: #43a047;
// }
// .card__media--autothumb.yellow {
// background-color: #ffeb3b;
// }
// .card__media--autothumb.orange {
// background-color: #ffa726;
// }
//
// .card__media--autothumb .card__autothumb__text {
// font-size: 2em;
// width: 100%;
// color: #ffffff;
// text-align: center;
// position: absolute;
// top: 36%;
// }
//
// .card--form {
// width: calc(var(--input-width) + var(--card-padding) * 2);
// }
//
.card-series-submit { //
margin-left: auto; // .card-series-submit {
margin-right: auto; // margin-left: auto;
max-width: var(--card-max-width); // margin-right: auto;
padding: $spacing-vertical / 2; // max-width: var(--card-max-width);
} // padding: $spacing-vertical / 2;
// }
/*
.card-row is used on the discover page
It is a list of cards that extend past the right edge of the screen
There are left/right arrows to scroll the cards and view hidden content
*/
.card-row { .card-row {
+ .card-row {
margin-top: $spacing-vertical * 1/3;
}
}
.card-row__placeholder {
padding-bottom: $spacing-vertical;
}
$padding-top-card-hover-hack: 20px;
$padding-right-card-hover-hack: 30px;
.card-row__items {
width: 100%;
overflow: hidden;
/*hacky way to give space for hover */
padding-top: $padding-top-card-hover-hack;
margin-top: -1 * $padding-top-card-hover-hack;
padding-right: $padding-right-card-hover-hack;
margin-right: -1 * $padding-right-card-hover-hack;
> .card {
vertical-align: top;
display: inline-block;
}
> .card + .card {
margin-left: 16px;
}
}
.card-row--small {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
width: 100%; width: 100%;
min-width: var(--card-small-width); min-width: var(--card-small-width);
margin-right: $spacing-vertical; padding-top: $spacing-vertical;
} }
.card-row__header { .card-row__header {
margin-bottom: 16px; display: flex;
flex-direction: row;
justify-content: space-between;
// specific padding-left styling is needed here
// this needs to be used on a page with noPadding
// doing so allows the content to scroll to the edge of the screen
padding-left: $spacing-vertical * 2/3;
}
.card-row__title {
display: flex;
align-items: center;
} }
.card-row__scrollhouse { .card-row__scrollhouse {
position: relative; padding-top: $spacing-vertical * 2/3;
/*hacky way to give space for hover */ overflow: hidden;
padding-right: $padding-right-card-hover-hack;
}
.card-row__nav { .card {
position: absolute; display: inline-block;
padding: 0 var(--card-margin); vertical-align: top;
height: 100%; margin-left: $spacing-vertical * 2/3;
top: calc($padding-top-card-hover-hack - var(--card-margin)); }
}
.card-row__nav .card-row__scroll-button { .card:last-of-type {
background: var(--card-bg); padding-right: $spacing-vertical * 2/3;
color: var(--color-help);
box-shadow: var(--box-shadow-layer);
padding: $spacing-vertical $spacing-vertical / 2;
position: absolute;
cursor: pointer;
left: 0;
top: 36%;
z-index: 2;
opacity: 0.8;
transition: transform 0.2s var(--animation-style);
&:hover {
opacity: 1;
transform: scale(calc(var(--card-link-scaling) * 1.1));
} }
}
.card-row__nav--left {
left: 0;
}
.card-row__nav--right {
right: 0;
} }
/* /*
if we keep doing things like this, we should add a real grid system, but I'm going to be a selective dick about it - Jeremy if we keep doing things like this, we should add a real grid system, but I'm going to be a selective dick about it - Jeremy
*/ */
.card-grid { //TODO: css grid
$margin-card-grid: $spacing-vertical * 2/3; // .card-grid {
display: flex; // $margin-card-grid: $spacing-vertical * 2/3;
flex-wrap: wrap; // display: flex;
> .card { // flex-wrap: wrap;
width: $width-page-constrained / 2 - $margin-card-grid / 2; // > .card {
flex-grow: 1; // width: $width-page-constrained / 2 - $margin-card-grid / 2;
} // flex-grow: 1;
> .card:nth-of-type(2n - 1):not(:last-child) { // }
margin-right: $margin-card-grid; // > .card:nth-of-type(2n - 1):not(:last-child) {
} // margin-right: $margin-card-grid;
} // }
// }

View file

@ -5,12 +5,6 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
// this shouldn't know about the card width
// will come back to this for the redesign - sean
.channel-name--small {
width: calc(var(--card-small-width) * 2 / 3);
}
.channel-indicator__icon--invalid { .channel-indicator__icon--invalid {
color: var(--color-error); color: var(--color-error);
} }

View file

@ -1,64 +1,60 @@
#header { #header {
color: var(--header-color);
background: var(--header-bg);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around;
position: fixed; position: fixed;
box-shadow: var(--box-shadow-layer);
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: var(--header-height);
z-index: 3; z-index: 3;
padding: $spacing-vertical / 2;
box-sizing: border-box; box-sizing: border-box;
} color: var(--header-color);
.header__item { background-color: var(--header-bg);
padding-left: $spacing-vertical / 4;
padding-right: $spacing-vertical / 4;
.button-alt {
background: var(--header-button-bg) !important;
font-size: 1em;
}
.button-alt:hover {
background: var(--header-button-hover-bg) !important;
}
} }
.header__item--wunderbar { .header__actions-left {
flex-grow: 1; display: flex;
padding: 0 5px;
} }
.wunderbar { .header__actions-right {
position: relative; margin-left: auto;
.icon {
position: absolute;
left: 10px;
top: $spacing-vertical / 2 - 4px; //hacked
}
} }
.wunderbar--active .icon-search { .header__wunderbar {
color: var(--color-primary); flex: 1;
max-width: 325px;
min-width: 175px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
height: 100%;
display: flex;
align-items: center;
padding: 10px 5px;
cursor: text;
} }
// below styles should be inside the common input styling
// will come back to this with the redesign - sean
.wunderbar__input { .wunderbar__input {
background: var(--search-bg); height: 50%;
width: 100%; width: 100%;
color: var(--search-color); color: var(--search-color);
height: $spacing-vertical * 1.5; padding: 10px;
line-height: $spacing-vertical * 1.5; background-color: #f3f3f3;
padding-left: 38px; border-radius: 10px;
padding-right: 5px; font-size: 0.9em;
border-radius: 2px;
border: var(--search-border);
transition: box-shadow var(--transition-duration) var(--transition-type);
&:focus { &:focus {
background: var(--search-active-bg); // TODO: focus style
color: var(--search-active-color);
box-shadow: var(--search-active-shadow);
border-color: var(--color-primary);
} }
} }
.wunderbar__suggestion {
padding: 5px;
background-color: var(--header-bg);
cursor: pointer;
}
.wunderbar__active-suggestion {
background-color: #a3ffb0;
}

View file

@ -2,10 +2,7 @@
.tooltip { .tooltip {
position: relative; position: relative;
} padding: 0 $spacing-vertical / 3;
.tooltip__link {
@include text-link();
} }
.tooltip__body { .tooltip__body {
@ -17,16 +14,15 @@
box-sizing: border-box; box-sizing: border-box;
padding: $spacing-vertical / 2; padding: $spacing-vertical / 2;
width: var(--tooltip-width); width: var(--tooltip-width);
border: var(--tooltip-border);
color: var(--tooltip-color); color: var(--tooltip-color);
background-color: var(--tooltip-bg); background-color: var(--tooltip-bg);
font-size: calc(var(--font-size) * 7/8); font-size: calc(var(--font-size) * 7/8);
line-height: var(--font-line-height); line-height: var(--font-line-height);
box-shadow: var(--box-shadow-layer); box-shadow: var(--box-shadow-layer);
border-radius: var(--card-radius);
} }
.tooltip--header .tooltip__link { .tooltip__link {
@include text-link(#aaa);
font-size: calc(var(--font-size) * 3/4); font-size: calc(var(--font-size) * 3/4);
margin-left: var(--button-padding); margin-left: var(--button-padding);
vertical-align: middle; vertical-align: middle;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -2661,6 +2661,10 @@ dom-converter@~0.1:
dependencies: dependencies:
utila "~0.3" utila "~0.3"
dom-scroll-into-view@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/dom-scroll-into-view/-/dom-scroll-into-view-1.2.1.tgz#e8f36732dd089b0201a88d7815dc3f88e6d66c7e"
dom-serializer@0: dom-serializer@0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"