Revert "Redesign groundwork, homepage, search" #934

Merged
neb-b merged 1 commit from revert-870-redesign-wip into master 2018-01-09 02:15:58 +01:00
66 changed files with 1466 additions and 2103 deletions
Showing only changes of commit 2b9561f0c0 - Show all commits

View file

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

View file

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

View file

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

View file

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

View file

@ -1,18 +1,48 @@
// @flow
import React from 'react';
type Props = {
thumbnail: ?string, // externally sourced image
};
class CardMedia extends React.PureComponent {
static AUTO_THUMB_CLASSES = [
'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() {
const { thumbnail } = this.props;
const { title, thumbnail } = this.props;
const atClass = this.state.autoThumbClass;
if (thumbnail) {
return <div className="card__media" style={{ backgroundImage: `url('${thumbnail}')` }} />;
}
return <div className="card__media card__media--autothumb">LBRY</div>;
return (
<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,6 +1,3 @@
// 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 PropTypes from 'prop-types';
import { formatCredits, formatFullPrice } from 'util/formatCredits';
@ -173,4 +170,3 @@ export class Thumbnail extends React.PureComponent {
);
}
}
/* eslint-enable */

View file

@ -1,255 +0,0 @@
// @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

@ -1,43 +0,0 @@
// @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

@ -1,57 +0,0 @@
// @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,101 +1,111 @@
// @flow
import React from 'react';
import lbryuri from 'lbryuri';
import lbryuri from 'lbryuri.js';
import CardMedia from 'component/cardMedia';
import Link from 'component/link';
import { TruncatedText } from 'component/common';
import Icon from 'component/common/icon';
import Icon from 'component/icon';
import FilePrice from 'component/filePrice';
import UriIndicator from 'component/uriIndicator';
import NsfwOverlay from 'component/nsfwOverlay';
import TruncatedMarkdown from 'component/truncatedMarkdown';
import * as icons from 'constants/icons';
import classnames from 'classnames';
// TODO: iron these out
type Props = {
isResolvingUri: boolean,
resolveUri: string => void,
uri: string,
claim: ?{ claim_id: string },
fileInfo: ?{},
metadata: ?{ nsfw: boolean, thumbnail: ?string },
navigate: (string, ?{}) => void,
rewardedContentClaimIds: Array<string>,
obscureNsfw: boolean,
};
class FileCard extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
hovered: false,
};
}
class FileCard extends React.PureComponent<Props> {
componentWillMount() {
this.resolve(this.props);
}
componentWillReceiveProps(nextProps: Props) {
componentWillReceiveProps(nextProps) {
this.resolve(nextProps);
}
resolve = (props: Props) => {
resolve(props) {
const { isResolvingUri, resolveUri, claim, uri } = props;
if (!isResolvingUri && claim === undefined && uri) {
resolveUri(uri);
}
};
}
handleMouseOver() {
this.setState({
hovered: true,
});
}
handleMouseOut() {
this.setState({
hovered: false,
});
}
render() {
const {
claim,
fileInfo,
metadata,
isResolvingUri,
navigate,
rewardedContentClaimIds,
obscureNsfw,
} = this.props;
const uri = lbryuri.normalize(this.props.uri);
const title = metadata && metadata.title ? metadata.title : uri;
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null;
const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw;
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
// Come back to this on other pages
// let description = '';
// if (isResolvingUri && !claim) {
// description = __('Loading...');
// } else if (metadata && metadata.description) {
// description = metadata.description;
// } else if (claim === null) {
// description = __('This address contains no content.');
// }
let description = '';
if (isResolvingUri && !claim) {
description = __('Loading...');
} else if (metadata && metadata.description) {
description = metadata.description;
} 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 (
<section
tabIndex="0"
role="button"
onClick={() => navigate('/show', { uri })}
className={classnames('card card--small card__link', {
'card--obscured': shouldObscureNsfw,
})}
className={`card card--small card--link ${obscureNsfw ? 'card--obscured ' : ''}`}
onMouseEnter={this.handleMouseOver.bind(this)}
onMouseLeave={this.handleMouseOut.bind(this)}
>
<CardMedia thumbnail={thumbnail} />
<div className="card__title-identity">
<div className="card__title--small">
<TruncatedText lines={3}>{title}</TruncatedText>
</div>
<div className="card__subtitle">
<UriIndicator uri={uri} link />
<div className="card--file-subtitle">
<FilePrice uri={uri} /> {isRewardContent && <Icon icon={icons.FEATURED} padded />}
{fileInfo && <Icon icon={icons.LOCAL} padded />}
<div className="card__inner">
<Link onClick={() => navigate('/show', { uri })} className="card__link">
<CardMedia title={title} thumbnail={thumbnail} />
<div className="card__title-identity">
<div className="card__title" title={title}>
<TruncatedText lines={1}>{title}</TruncatedText>
</div>
<div className="card__subtitle">
<span className="card__indicators card--file-subtitle">
<FilePrice uri={uri} />{' '}
{isRewardContent && <Icon icon={icons.FEATURED} leftPad />}{' '}
{fileInfo && <Icon icon={icons.LOCAL} leftPad />}
</span>
<span className="card--file-subtitle">
<UriIndicator uri={uri} link span smallCard />
</span>
</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 />}
{obscureNsfw && this.state.hovered && <NsfwOverlay />}
</section>
);
/* eslint-enable jsx-a11y/click-events-have-key-events */
}
}

View file

@ -1,6 +1,6 @@
import React from 'react';
import { BusyMessage } from 'component/common';
import Icon from 'component/common/icon';
import Icon from 'component/icon';
import Link from 'component/link';
class FileDownloadLink extends React.PureComponent {

View file

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

View file

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import FormField from 'component/formField';
import Icon from 'component/common/icon';
import Icon from 'component/icon';
let formFieldCounter = 0;

View file

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

View file

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

View file

@ -0,0 +1,50 @@
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,5 +1,5 @@
import React from 'react';
import Icon from 'component/common/icon';
import Icon from 'component/icon';
import RewardLink from 'component/rewardLink';
import rewards from 'rewards.js';

View file

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

View file

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'component/common/icon';
import Icon from 'component/icon';
import Link from 'component/link';
export class DropDownMenuItem extends React.PureComponent {

View file

@ -1,9 +0,0 @@
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

@ -1,26 +0,0 @@
// @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

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

View file

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

View file

@ -1,601 +0,0 @@
/*
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,116 +1,166 @@
// @flow
import React from 'react';
import lbryuri from 'lbryuri';
import classnames from 'classnames';
import Autocomplete from './internal/autocomplete';
import PropTypes from 'prop-types';
import lbryuri from 'lbryuri.js';
import Icon from 'component/icon';
import { parseQueryParams } from 'util/query_params';
type Props = {
updateSearchQuery: string => void,
getSearchSuggestions: string => void,
onSearch: string => void,
onSubmit: string => void,
searchQuery: ?string,
isActive: boolean,
address: ?string,
suggestions: Array<string>,
};
class WunderBar extends React.PureComponent {
static TYPING_TIMEOUT = 800;
class WunderBar extends React.PureComponent<Props> {
constructor() {
super();
(this: any).handleSubmit = this.handleSubmit.bind(this);
(this: any).handleChange = this.handleChange.bind(this);
(this: any).focus = this.focus.bind(this);
this.input = undefined;
static propTypes = {
onSearch: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
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,
};
}
input: ?HTMLInputElement;
handleChange(e: SyntheticInputEvent<*>) {
const { updateSearchQuery, getSearchSuggestions } = this.props;
const { value } = e.target;
updateSearchQuery(value);
getSearchSuggestions(value);
}
focus() {
const { input } = this;
if (input) {
input.focus();
componentWillUnmount() {
if (this.userTypingTimer) {
clearTimeout(this._userTypingTimer);
}
}
handleSubmit(value: string) {
if (!value) {
return;
onChange(event) {
if (this._userTypingTimer) {
clearTimeout(this._userTypingTimer);
}
const { onSubmit, onSearch } = this.props;
this.setState({ address: event.target.value });
// if they choose the "search for {value}" in the suggestions
// it will contain the {query}?search
const choseDoSuggestedSearch = value.endsWith('?search');
this._isSearchDispatchPending = true;
let searchValue = value;
if (choseDoSuggestedSearch) {
searchValue = value.slice(0, -7); // trim off ?search
const searchQuery = event.target.value;
this._userTypingTimer = setTimeout(() => {
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 (
nextProps.viewingPage !== this.props.viewingPage ||
nextProps.address != this.props.address
) {
this.setState({ address: nextProps.address, icon: nextProps.icon });
}
}
if (this.input) {
this.input.blur();
}
onFocus() {
this._stateBeforeSearch = this.state;
const newState = {
icon: 'icon-search',
isActive: true,
};
try {
const uri = lbryuri.normalize(value);
onSubmit(uri);
} catch (e) {
// search query isn't a valid uri
onSearch(searchValue);
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 {
uri = lbryuri.normalize(value);
this.setState({ value: uri });
} catch (error) {
// then it's not a valid URL, so let's search
uri = value;
method = 'onSearch';
}
this.props[method](uri, extraParams);
this._input.blur();
}
}
onReceiveRef(ref) {
this._input = ref;
}
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 (
<div
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
{...props}
className="wunderbar__input"
placeholder="Search for videos, music, games and more"
/>
)}
renderItem={(item, isHighlighted) => (
<div
key={item.value}
className={classnames('wunderbar__suggestion', {
'wunderbar__active-suggestion': isHighlighted,
})}
>
{item.label}
</div>
)}
<div className={`wunderbar${this.state.isActive ? ' wunderbar--active' : ''}`}>
{this.state.icon ? <Icon fixed icon={this.state.icon} /> : ''}
<input
className="wunderbar__input"
type="search"
ref={this.onReceiveRef}
onFocus={this.onFocus}
onBlur={this.onBlur}
onChange={this.onChange}
onKeyPress={this.onKeyPress}
value={this.state.address}
placeholder={__('Find videos, music, games, and more')}
/>
</div>
);

View file

@ -88,13 +88,9 @@ export const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED';
export const FILE_DELETE = 'FILE_DELETE';
// Search
export const SEARCH_START = 'SEARCH_START';
export const SEARCH_SUCCESS = 'SEARCH_SUCCESS';
export const SEARCH_FAIL = 'SEARCH_FAIL';
export const UPDATE_SEARCH_QUERY = 'UPDATE_SEARCH_QUERY';
export const SEARCH_SUGGESTIONS_START = 'SEARCH_SUGGESTIONS_START';
export const GET_SEARCH_SUGGESTIONS_SUCCESS = 'GET_SEARCH_SUGGESTIONS_SUCCESS';
export const GET_SEARCH_SUGGESTIONS_FAIL = 'GET_SEARCH_SUGGESTIONS_FAIL';
export const SEARCH_STARTED = 'SEARCH_STARTED';
export const SEARCH_COMPLETED = 'SEARCH_COMPLETED';
export const SEARCH_CANCELLED = 'SEARCH_CANCELLED';
// Settings
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';

View file

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

View file

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

View file

@ -1,37 +1,259 @@
// @flow
import React from 'react';
import Page from 'component/page';
import CategoryList from 'component/common/category-list';
import ReactDOM from 'react-dom';
import lbryuri from 'lbryuri';
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';
type Props = {
fetchFeaturedUris: () => void,
fetchingFeaturedUris: boolean,
featuredUris: {},
};
// This should be in a separate file
export class FeaturedCategory extends React.PureComponent {
constructor() {
super();
class DiscoverPage extends React.PureComponent<Props> {
this.state = {
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() {
this.props.fetchFeaturedUris();
}
render() {
const { featuredUris, fetchingFeaturedUris } = this.props;
const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length;
const failedToLoad = !fetchingFeaturedUris && !hasContent;
const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length,
failedToLoad = !fetchingFeaturedUris && !hasContent;
return (
<Page noPadding isLoading={!hasContent && fetchingFeaturedUris}>
<main
className={classnames('main main--no-margin', {
reloading: hasContent && fetchingFeaturedUris,
})}
>
<SubHeader fullWidth smallMargin />
{!hasContent && fetchingFeaturedUris && <BusyMessage message={__('Fetching content')} />}
{hasContent &&
Object.keys(featuredUris).map(
category =>
featuredUris[category].length ? (
<CategoryList key={category} category={category} names={featuredUris[category]} />
<FeaturedCategory
key={category}
category={category}
names={featuredUris[category]}
/>
) : (
''
)
)}
{failedToLoad && <div className="empty">{__('Failed to load landing content.')}</div>}
</Page>
</main>
);
}
}

View file

@ -1,4 +1,3 @@
/* eslint-disable */
import React from 'react';
import lbry from 'lbry';
import lbryuri from 'lbryuri';
@ -7,13 +6,12 @@ import { Thumbnail } from 'component/common';
import FilePrice from 'component/filePrice';
import FileDetails from 'component/fileDetails';
import UriIndicator from 'component/uriIndicator';
import Icon from 'component/common/icon';
import Icon from 'component/icon';
import WalletSendTip from 'component/walletSendTip';
import DateTime from 'component/dateTime';
import * as icons from 'constants/icons';
import Link from 'component/link';
import SubscribeButton from 'component/subscribeButton';
import Page from 'component/page';
class FilePage extends React.PureComponent {
componentDidMount() {
@ -71,52 +69,49 @@ class FilePage extends React.PureComponent {
}
return (
<Page>
<section className={`card ${obscureNsfw ? 'card--obscured ' : ''}`}>
<div className="show-page-media">
{isPlayable ? (
<Video className="video-embedded" uri={uri} />
) : metadata && metadata.thumbnail ? (
<Thumbnail src={metadata.thumbnail} />
) : (
<Thumbnail />
)}
</div>
<div className="card__inner">
{(!tab || tab === 'details') && (
<div>
{' '}
<div className="card__title-identity">
{!fileInfo || fileInfo.written_bytes <= 0 ? (
<span style={{ float: 'right' }}>
<FilePrice uri={lbryuri.normalize(uri)} />
{isRewardContent && (
<span>
{' '}
<Icon icon={icons.FEATURED} />
</span>
)}
</span>
) : null}
<h1>{title}</h1>
<div className="card__subtitle card--file-subtitle">
<UriIndicator uri={uri} link />
<span className="card__publish-date">
Published on <DateTime block={height} show={DateTime.SHOW_DATE} />
</span>
</div>
<section className={`card ${obscureNsfw ? 'card--obscured ' : ''}`}>
<div className="show-page-media">
{isPlayable ? (
<Video className="video-embedded" uri={uri} />
) : metadata && metadata.thumbnail ? (
<Thumbnail src={metadata.thumbnail} />
) : (
<Thumbnail />
)}
</div>
<div className="card__inner">
{(!tab || tab === 'details') && (
<div>
{' '}
<div className="card__title-identity">
{!fileInfo || fileInfo.written_bytes <= 0 ? (
<span style={{ float: 'right' }}>
<FilePrice uri={lbryuri.normalize(uri)} />
{isRewardContent && (
<span>
{' '}
<Icon icon={icons.FEATURED} />
</span>
)}
</span>
) : null}
<h1>{title}</h1>
<div className="card__subtitle card--file-subtitle">
<UriIndicator uri={uri} link />
<span className="card__publish-date">
Published on <DateTime block={height} show={DateTime.SHOW_DATE} />
</span>
</div>
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
<FileDetails uri={uri} />
</div>
)}
{tab === 'tip' && <WalletSendTip claim_id={claim.claim_id} uri={uri} />}
</div>
</section>
</Page>
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
<FileDetails uri={uri} />
</div>
)}
{tab === 'tip' && <WalletSendTip claim_id={claim.claim_id} uri={uri} />}
</div>
</section>
);
}
}
export default FilePage;
/* eslint-enable */

View file

@ -4,7 +4,7 @@ import lbry from 'lbry.js';
import Link from 'component/link';
import SubHeader from 'component/subHeader';
import { BusyMessage } from 'component/common';
import Icon from 'component/common/icon';
import Icon from 'component/icon';
class HelpPage extends React.PureComponent {
constructor(props) {

View file

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

View file

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

View file

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

View file

@ -5,119 +5,63 @@ import { doNavigate } from 'redux/actions/navigation';
import { selectCurrentPage } from 'redux/selectors/navigation';
import batchActions from 'util/batchActions';
// TODO: this should be in a util
const handleSearchApiResponse = searchResponse =>
searchResponse.status === 200
? Promise.resolve(searchResponse.json())
: Promise.reject(new Error(searchResponse.statusText));
// eslint-disable-next-line import/prefer-default-export
export function doSearch(rawQuery) {
return (dispatch, getState) => {
const state = getState();
const page = selectCurrentPage(state);
export const doSearch = rawQuery => (dispatch, getState) => {
const state = getState();
const page = selectCurrentPage(state);
const query = rawQuery.replace(/^lbry:\/\//i, '');
const query = rawQuery.replace(/^lbry:\/\//i, '');
if (!query) {
dispatch({
type: ACTIONS.SEARCH_FAIL,
});
return;
}
dispatch({
type: ACTIONS.SEARCH_START,
data: { query },
});
if (page !== 'search') {
dispatch(doNavigate('search', { query }));
} else {
fetch(`https://lighthouse.lbry.io/search?s=${query}`)
.then(handleSearchApiResponse)
.then(data => {
const uris = [];
const actions = [];
data.forEach(result => {
const uri = Lbryuri.build({
name: result.name,
claimId: result.claimId,
});
actions.push(doResolveUri(uri));
uris.push(uri);
});
actions.push({
type: ACTIONS.SEARCH_SUCCESS,
data: {
query,
uris,
},
});
dispatch(batchActions(...actions));
})
.catch(() => {
dispatch({
type: ACTIONS.SEARCH_FAIL,
});
});
}
};
export const updateSearchQuery = searchQuery => ({
type: ACTIONS.UPDATE_SEARCH_QUERY,
data: { searchQuery },
});
export const getSearchSuggestions = value => dispatch => {
dispatch({ type: ACTIONS.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(handleSearchApiResponse)
.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 =>
if (!query) {
dispatch({
type: ACTIONS.GET_SEARCH_SUGGESTIONS_FAIL,
data: err,
})
);
};
type: ACTIONS.SEARCH_CANCELLED,
});
return;
}
dispatch({
type: ACTIONS.SEARCH_STARTED,
data: { query },
});
if (page !== 'search') {
dispatch(doNavigate('search', { query }));
} else {
fetch(`https://lighthouse.lbry.io/search?s=${query}`)
.then(
response =>
response.status === 200
? Promise.resolve(response.json())
: Promise.reject(new Error(response.statusText))
)
.then(data => {
const uris = [];
const actions = [];
data.forEach(result => {
const uri = Lbryuri.build({
name: result.name,
claimId: result.claimId,
});
actions.push(doResolveUri(uri));
uris.push(uri);
});
actions.push({
type: ACTIONS.SEARCH_COMPLETED,
data: {
query,
uris,
},
});
dispatch(batchActions(...actions));
})
.catch(() => {
dispatch({
type: ACTIONS.SEARCH_CANCELLED,
});
});
}
};
}

View file

@ -1,75 +1,32 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { handleActions } from 'util/redux-utils';
type SearchState = {
isActive: boolean,
searchQuery: string,
searchingForSuggestions: boolean,
suggestions: Array<string>,
urisByQuery: {},
};
const reducers = {};
const defaultState = {
isActive: false,
searchQuery: '', // needs to be an empty string for input focusing
searchingForSuggestions: false,
suggestions: [],
urisByQuery: {},
searching: false,
};
export default handleActions(
{
[ACTIONS.SEARCH_START]: (state: SearchState): SearchState => ({
...state,
searching: true,
}),
[ACTIONS.SEARCH_SUCCESS]: (state: SearchState, action): SearchState => {
const { query, uris } = action.data;
reducers[ACTIONS.SEARCH_STARTED] = state =>
Object.assign({}, state, {
searching: true,
});
return {
...state,
searching: false,
urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }),
};
},
reducers[ACTIONS.SEARCH_COMPLETED] = (state, action) => {
const { query, uris } = action.data;
[ACTIONS.SEARCH_FAIL]: (state: SearchState): SearchState => ({
...state,
searching: false,
}),
return Object.assign({}, state, {
searching: false,
urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }),
});
};
[ACTIONS.UPDATE_SEARCH_QUERY]: (state: SearchState, action): SearchState => ({
...state,
searchQuery: action.data.searchQuery,
suggestions: [],
isActive: true,
}),
reducers[ACTIONS.SEARCH_CANCELLED] = state =>
Object.assign({}, state, {
searching: false,
});
[ACTIONS.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
);
export default function reducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);
return state;
}

View file

@ -68,6 +68,30 @@ export const selectPageTitle = createSelector(
selectCurrentParams,
(page, params) => {
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': {
const parts = [Lbryuri.normalize(params.uri)];
// If the params has any keys other than "uri"
@ -76,14 +100,21 @@ export const selectPageTitle = createSelector(
}
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':
return __('Discover');
case false:
case null:
case '':
return '';
default:
return '';
return page[0].toUpperCase() + (page.length > 0 ? page.substr(1) : '');
}
}
);

View file

@ -28,15 +28,52 @@ export const selectWunderBarAddress = createSelector(
selectCurrentPage,
selectPageTitle,
selectSearchQuery,
(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;
}
(page, title, query) => (page !== 'search' ? title : query || title)
);
return '';
export const selectWunderBarIcon = createSelector(
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,30 +1,4 @@
// Generic html styles used accross the App
// component specific styling should go in the component scss file
// The actual fonts used will change ex: medium vs regular
@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: 800;
font-style: normal;
text-rendering: optimizeLegibility;
src: url('../../../static/font/metropolis/Metropolis-ExtraBold.woff2') format('woff2');
}
@import url(https://fonts.googleapis.com/css?family=Roboto:400,400i,500,500i,700);
html {
height: 100%;
@ -33,20 +7,84 @@ html {
body {
color: var(--text-color);
font-family: 'Metropolis', sans-serif;
font-family: 'Roboto', sans-serif;
line-height: var(--font-line-height);
height: 100%;
overflow: hidden;
}
h1,
h2,
h3,
h4,
h5 {
/* Custom text selection */
*::selection {
background: var(--text-selection-bg);
color: var(--text-selection-color);
}
#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;
}
#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 {
font-size: 1.75em;
}
@ -62,13 +100,11 @@ h4 {
h5 {
font-size: 1.1em;
}
sup,
sub {
vertical-align: baseline;
position: relative;
}
sup {
top: -0.4em;
}
@ -81,67 +117,11 @@ code {
background-color: var(--color-bg-alt);
}
// Without this buttons don't have the Metropolis font
button {
font-family: inherit;
}
#window {
height: 100%;
overflow: hidden;
}
#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);
// TODO: fix this scrollbar extends beyond screen at the bottom
padding-bottom: var(--header-height);
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;
p {
margin-bottom: 0.8em;
&:last-child {
margin-bottom: 0;
}
}
.hidden {
@ -212,3 +192,7 @@ button {
section.section-spaced {
margin-bottom: $spacing-vertical;
}
.text-center {
text-align: center;
}

View file

@ -27,16 +27,6 @@
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 */
.icon-rocket {
color: orangered;

View file

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

View file

@ -1,78 +1,89 @@
/*
TODO:
Determine [disabled] or .disabled
Add <a> support (probably just get rid of button prefix)
*/
@import '../mixin/link.scss';
button {
border: none;
text-decoration: none;
cursor: pointer;
$button-focus-shift: 12%;
.button-set-item {
position: relative;
}
display: inline-block;
button:disabled.btn--disabled {
cursor: default;
background-color: transparent;
}
button.btn {
padding: 10px;
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);
+ .button-set-item {
margin-left: var(--button-intra-margin);
}
}
button.btn.btn--alt {
color: var(--btn-alt-color);
background-color: #efefef;
&:hover {
color: #111;
.button-block,
.faux-button-block {
display: inline-block;
height: var(--button-height);
line-height: var(--button-height);
text-decoration: none;
border: 0 none;
text-align: center;
border-radius: var(--button-radius);
text-transform: uppercase;
.icon {
top: 0em;
}
&:active {
background-color: #cdcdcd;
.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;
font-weight: 500;
font-size: 14px;
user-select: none;
transition: background var(--animation-duration) var(--animation-style);
}
&:disabled {
color: var(--color-help);
background-color: transparent;
.button__content {
margin: 0 var(--button-padding);
display: flex;
.link-label {
text-decoration: none !important;
}
}
button.btn.btn--circle {
border-radius: 50%;
transition: all 0.2s;
.button-primary {
color: var(--button-primary-color);
background-color: var(--button-primary-bg);
box-shadow: var(--box-shadow-layer);
&:hover:not([disabled]) {
border-radius: var(--btn-radius);
&:focus {
//color: var(--button-primary-active-color);
//background-color:color: var(--button-primary-active-bg);
//box-shadow: $box-shadow-focus;
}
}
button.btn.btn--inverse {
box-shadow: none;
background-color: transparent;
color: var(--btn-primary-bg);
.button-alt {
background-color: var(--button-bg);
box-shadow: var(--box-shadow-layer);
}
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
.button-text {
@include text-link();
display: inline-block;
&:hover {
border-bottom: 1px solid;
.button__content {
margin: 0 var(--text-link-padding);
}
}
.btn__label {
padding: 0 5px;
.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,19 +2,194 @@
margin-left: auto;
margin-right: auto;
max-width: var(--card-max-width);
background: var(--card-bg);
box-shadow: var(--box-shadow-layer);
border-radius: var(--card-radius);
margin-bottom: var(--card-margin);
overflow: auto;
user-select: text;
display: flex;
//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--placeholder {
background-color: black;
.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;
align-items: center;
font-weight: 600;
}
.card__link {
display: block;
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 {
width: var(--card-small-width);
min-height: var(--card-small-width);
overflow-x: hidden;
white-space: normal;
}
@ -22,237 +197,126 @@
height: calc(var(--card-small-width) * 9 / 16);
}
.card__link {
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--form {
width: calc(var(--input-width) + var(--card-padding) * 2);
}
.card__subtitle {
color: var(--color-help);
font-size: 0.85em;
padding-top: $spacing-vertical * 1/3;
line-height: calc(var(--font-line-height) * 1 / 0.85);
}
// .card__title-primary .meta {
// white-space: nowrap;
// overflow: hidden;
// text-overflow: ellipsis;
// }
//
.card--file-subtitle {
display: flex;
}
//
// .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;
// 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);
// }
//
// this is too specific
// it should be a helper class
// ex. ".m-padding-left"
// will come back to this during the redesign - sean
.card__publish-date {
padding-left: 20px;
}
//
// .card-series-submit {
// margin-left: auto;
// margin-right: auto;
// max-width: var(--card-max-width);
// padding: $spacing-vertical / 2;
// }
.card-series-submit {
margin-left: auto;
margin-right: auto;
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 {
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;
white-space: nowrap;
width: 100%;
min-width: var(--card-small-width);
padding-top: $spacing-vertical;
margin-right: $spacing-vertical;
}
.card-row__header {
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;
margin-bottom: 16px;
}
.card-row__scrollhouse {
padding-top: $spacing-vertical * 2/3;
overflow: hidden;
position: relative;
/*hacky way to give space for hover */
padding-right: $padding-right-card-hover-hack;
}
.card {
display: inline-block;
vertical-align: top;
margin-left: $spacing-vertical * 2/3;
}
.card-row__nav {
position: absolute;
padding: 0 var(--card-margin);
height: 100%;
top: calc($padding-top-card-hover-hack - var(--card-margin));
}
.card-row__nav .card-row__scroll-button {
background: var(--card-bg);
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);
.card:last-of-type {
padding-right: $spacing-vertical * 2/3;
&: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
*/
//TODO: css grid
// .card-grid {
// $margin-card-grid: $spacing-vertical * 2/3;
// display: flex;
// flex-wrap: wrap;
// > .card {
// 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-grid {
$margin-card-grid: $spacing-vertical * 2/3;
display: flex;
flex-wrap: wrap;
> .card {
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;
}
}

View file

@ -5,6 +5,12 @@
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 {
color: var(--color-error);
}

View file

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

View file

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

View file

@ -2661,10 +2661,6 @@ dom-converter@~0.1:
dependencies:
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:
version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"