Revert "Redesign groundwork, homepage, search" #934
66 changed files with 1466 additions and 2103 deletions
|
@ -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]
|
||||
|
|
3
flow-typed/react-modal.js
vendored
3
flow-typed/react-modal.js
vendored
|
@ -1,3 +0,0 @@
|
|||
declare module 'react-modal' {
|
||||
declare module.exports: any;
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
5
src/renderer/component/icon/index.js
Normal file
5
src/renderer/component/icon/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Icon from './view';
|
||||
|
||||
export default connect(null, null)(Icon);
|
50
src/renderer/component/icon/view.jsx
Normal file
50
src/renderer/component/icon/view.jsx
Normal 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} />;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
54
src/renderer/component/tooltip.js
Normal file
54
src/renderer/component/tooltip.js
Normal 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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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')
|
||||
);
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) : '');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue