Redesign groundwork (home/search)
This commit is contained in:
parent
d2011551a9
commit
7d492ae1fc
61 changed files with 2151 additions and 1445 deletions
|
@ -18,5 +18,6 @@ module.name_mapper='^types\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/types\1'
|
|||
module.name_mapper='^component\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/component\1'
|
||||
module.name_mapper='^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
Normal file
3
flow-typed/react-modal.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
declare module 'react-modal' {
|
||||
declare module.exports: any;
|
||||
}
|
25
npm-debug.log.2899857694
Normal file
25
npm-debug.log.2899857694
Normal file
|
@ -0,0 +1,25 @@
|
|||
0 info it worked if it ends with ok
|
||||
1 verbose cli [ '/Users/seanyesmunt/.nvm/versions/node/v6.12.0/bin/node',
|
||||
1 verbose cli '/Users/seanyesmunt/.nvm/versions/node/v6.12.0/bin/npm',
|
||||
1 verbose cli 'config',
|
||||
1 verbose cli '--loglevel=warn',
|
||||
1 verbose cli 'get',
|
||||
1 verbose cli 'prefix' ]
|
||||
2 info using npm@3.10.10
|
||||
3 info using node@v6.12.0
|
||||
4 verbose exit [ 0, true ]
|
||||
5 verbose stack Error: write EPIPE
|
||||
5 verbose stack at exports._errnoException (util.js:1020:11)
|
||||
5 verbose stack at WriteWrap.afterWrite (net.js:800:14)
|
||||
6 verbose cwd /Users/seanyesmunt/Workspace/lbry/lbry-app
|
||||
7 error Darwin 17.2.0
|
||||
8 error argv "/Users/seanyesmunt/.nvm/versions/node/v6.12.0/bin/node" "/Users/seanyesmunt/.nvm/versions/node/v6.12.0/bin/npm" "config" "--loglevel=warn" "get" "prefix"
|
||||
9 error node v6.12.0
|
||||
10 error npm v3.10.10
|
||||
11 error code EPIPE
|
||||
12 error errno EPIPE
|
||||
13 error syscall write
|
||||
14 error write EPIPE
|
||||
15 error If you need help, you may report this error at:
|
||||
15 error <https://github.com/npm/npm/issues>
|
||||
16 verbose exit [ 1, true ]
|
|
@ -32,6 +32,7 @@
|
|||
"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,3 +1,4 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Router from 'component/router/index';
|
||||
import Header from 'component/header';
|
||||
|
@ -6,61 +7,83 @@ import ModalRouter from 'modal/modalRouter';
|
|||
import ReactModal from 'react-modal';
|
||||
import throttle from 'util/throttle';
|
||||
|
||||
class App extends React.PureComponent {
|
||||
type Props = {
|
||||
alertError: (string | {}) => void,
|
||||
recordScroll: number => void,
|
||||
currentStackIndex: number,
|
||||
currentPageAttributes: { path: string, scrollY: number },
|
||||
pageTitle: ?string,
|
||||
};
|
||||
|
||||
class App extends React.PureComponent<Props> {
|
||||
constructor() {
|
||||
super();
|
||||
this.mainContent = undefined;
|
||||
(this: any).scrollListener = this.scrollListener.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const { alertError } = this.props;
|
||||
|
||||
document.addEventListener('unhandledError', event => {
|
||||
// TODO: create type for this object
|
||||
// it lives in jsonrpc.js
|
||||
document.addEventListener('unhandledError', (event: any) => {
|
||||
alertError(event.detail);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { recordScroll } = this.props;
|
||||
const mainContent = document.getElementById('main-content');
|
||||
this.mainContent = mainContent;
|
||||
|
||||
const scrollListener = () => recordScroll(this.mainContent.scrollTop);
|
||||
|
||||
this.mainContent.addEventListener('scroll', throttle(scrollListener, 750));
|
||||
if (this.mainContent) {
|
||||
this.mainContent.addEventListener('scroll', throttle(this.scrollListener, 750));
|
||||
}
|
||||
|
||||
ReactModal.setAppElement('#window'); // fuck this
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mainContent.removeEventListener('scroll', this.scrollListener);
|
||||
componentWillReceiveProps(props: Props) {
|
||||
const { pageTitle } = props;
|
||||
this.setTitleFromProps(pageTitle);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this.setTitleFromProps(props);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { currentStackIndex: prevStackIndex } = prevProps;
|
||||
const { currentStackIndex, currentPageAttributes } = this.props;
|
||||
|
||||
if (currentStackIndex !== prevStackIndex) {
|
||||
if (this.mainContent && currentStackIndex !== prevStackIndex) {
|
||||
this.mainContent.scrollTop = currentPageAttributes.scrollY || 0;
|
||||
}
|
||||
}
|
||||
|
||||
setTitleFromProps(props) {
|
||||
window.document.title = props.pageTitle || 'LBRY';
|
||||
componentWillUnmount() {
|
||||
if (this.mainContent) {
|
||||
// having issues with this
|
||||
// $FlowFixMe
|
||||
this.mainContent.removeEventListener('scroll');
|
||||
}
|
||||
}
|
||||
|
||||
setTitleFromProps = (title: ?string) => {
|
||||
window.document.title = title || 'LBRY';
|
||||
};
|
||||
|
||||
scrollListener() {
|
||||
const { recordScroll } = this.props;
|
||||
if (this.mainContent) {
|
||||
recordScroll(this.mainContent.scrollTop);
|
||||
}
|
||||
}
|
||||
|
||||
mainContent: ?HTMLElement;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="window">
|
||||
<Theme />
|
||||
<Header />
|
||||
<div id="main-content">
|
||||
<Router />
|
||||
</div>
|
||||
<Router />
|
||||
<ModalRouter />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,48 +1,18 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
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)
|
||||
],
|
||||
});
|
||||
}
|
||||
type Props = {
|
||||
thumbnail: ?string, // externally sourced image
|
||||
};
|
||||
|
||||
class CardMedia extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { title, thumbnail } = this.props;
|
||||
const atClass = this.state.autoThumbClass;
|
||||
|
||||
const { thumbnail } = this.props;
|
||||
if (thumbnail) {
|
||||
return <div className="card__media" style={{ backgroundImage: `url('${thumbnail}')` }} />;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
return <div className="card__media card__media--autothumb">LBRY</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// just disabling the linter because this file shouldn't even exist
|
||||
// will continue to move components over to /components/common/{comp} - sean
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { formatCredits, formatFullPrice } from 'util/formatCredits';
|
||||
|
@ -170,3 +173,4 @@ export class Thumbnail extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
|
255
src/renderer/component/common/category-list.jsx
Normal file
255
src/renderer/component/common/category-list.jsx
Normal file
|
@ -0,0 +1,255 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import lbryuri from 'lbryuri';
|
||||
import ToolTip from 'component/common/tooltip';
|
||||
import FileCard from 'component/fileCard';
|
||||
import Button from 'component/link';
|
||||
|
||||
type Props = {
|
||||
category: string,
|
||||
names: Array<string>,
|
||||
categoryLink?: string,
|
||||
};
|
||||
|
||||
type State = {
|
||||
canScrollNext: boolean,
|
||||
canScrollPrevious: boolean,
|
||||
};
|
||||
|
||||
class CategoryList extends React.PureComponent<Props, State> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
canScrollPrevious: false,
|
||||
canScrollNext: false,
|
||||
};
|
||||
|
||||
(this: any).handleScrollNext = this.handleScrollNext.bind(this);
|
||||
(this: any).handleScrollPrevious = this.handleScrollPrevious.bind(this);
|
||||
this.rowItems = undefined;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const cardRow = this.rowItems;
|
||||
if (cardRow) {
|
||||
const cards = cardRow.getElementsByTagName('section');
|
||||
const lastCard = cards[cards.length - 1];
|
||||
const isCompletelyVisible = this.isCardVisible(lastCard);
|
||||
|
||||
if (!isCompletelyVisible) {
|
||||
// not sure how we can avoid doing this
|
||||
/* eslint-disable react/no-did-mount-set-state */
|
||||
this.setState({
|
||||
canScrollNext: true,
|
||||
});
|
||||
/* eslint-enable react/no-did-mount-set-state */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rowItems: ?HTMLDivElement;
|
||||
|
||||
handleScroll(cardRow: HTMLDivElement, scrollTarget: number) {
|
||||
const cards = cardRow.getElementsByTagName('section');
|
||||
const animationCallback = () => {
|
||||
const firstCard = cards[0];
|
||||
const lastCard = cards[cards.length - 1];
|
||||
const firstCardVisible = this.isCardVisible(firstCard);
|
||||
const lastCardVisible = this.isCardVisible(lastCard);
|
||||
this.setState({
|
||||
canScrollNext: !lastCardVisible,
|
||||
canScrollPrevious: !firstCardVisible,
|
||||
});
|
||||
};
|
||||
|
||||
const currentScrollLeft = cardRow.scrollLeft;
|
||||
const direction = currentScrollLeft > scrollTarget ? 'left' : 'right';
|
||||
this.scrollCardsAnimated(cardRow, scrollTarget, direction, animationCallback);
|
||||
}
|
||||
|
||||
scrollCardsAnimated = (
|
||||
cardRow: HTMLDivElement,
|
||||
scrollTarget: number,
|
||||
direction: string,
|
||||
callback: () => any
|
||||
) => {
|
||||
let start;
|
||||
const step = timestamp => {
|
||||
if (!start) start = timestamp;
|
||||
|
||||
const currentLeftVal = cardRow.scrollLeft;
|
||||
|
||||
let newTarget;
|
||||
let shouldContinue;
|
||||
let progress = currentLeftVal;
|
||||
|
||||
if (direction === 'right') {
|
||||
progress += timestamp - start;
|
||||
newTarget = Math.min(progress, scrollTarget);
|
||||
shouldContinue = newTarget < scrollTarget;
|
||||
} else {
|
||||
progress -= timestamp - start;
|
||||
newTarget = Math.max(progress, scrollTarget);
|
||||
shouldContinue = newTarget > scrollTarget;
|
||||
}
|
||||
|
||||
cardRow.scrollLeft = newTarget; // eslint-disable-line no-param-reassign
|
||||
|
||||
if (shouldContinue) {
|
||||
window.requestAnimationFrame(step);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
window.requestAnimationFrame(step);
|
||||
};
|
||||
|
||||
// check if a card is fully visible horizontally
|
||||
isCardVisible = (section: HTMLElement) => {
|
||||
const rect = section.getBoundingClientRect();
|
||||
const isVisible = rect.left >= 0 && rect.right <= window.innerWidth;
|
||||
return isVisible;
|
||||
};
|
||||
|
||||
handleScrollNext() {
|
||||
const cardRow = this.rowItems;
|
||||
if (cardRow) {
|
||||
const cards = cardRow.getElementsByTagName('section');
|
||||
|
||||
// loop over items until we find one that is on the screen
|
||||
// continue searching until a card isn't fully visible, this is the new target
|
||||
let firstFullVisibleCard;
|
||||
let firstSemiVisibleCard;
|
||||
|
||||
for (let i = 0; i < cards.length; i += 1) {
|
||||
const currentCardVisible = this.isCardVisible(cards[i]);
|
||||
|
||||
if (firstFullVisibleCard && !currentCardVisible) {
|
||||
firstSemiVisibleCard = cards[i];
|
||||
break;
|
||||
} else if (currentCardVisible) {
|
||||
[firstFullVisibleCard] = cards;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstFullVisibleCard && firstSemiVisibleCard) {
|
||||
const scrollTarget = firstSemiVisibleCard.offsetLeft - firstFullVisibleCard.offsetLeft;
|
||||
this.handleScroll(cardRow, scrollTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleScrollPrevious() {
|
||||
const cardRow = this.rowItems;
|
||||
if (cardRow) {
|
||||
const cards = cardRow.getElementsByTagName('section');
|
||||
|
||||
let hasFoundCard;
|
||||
let numberOfCardsThatCanFit = 0;
|
||||
|
||||
// loop starting at the end until we find a visible card
|
||||
// then count to find how many cards can fit on the screen
|
||||
for (let i = cards.length - 1; i >= 0; i -= 1) {
|
||||
const currentCard = cards[i];
|
||||
const isCurrentCardVisible = this.isCardVisible(currentCard);
|
||||
|
||||
if (isCurrentCardVisible) {
|
||||
if (!hasFoundCard) {
|
||||
hasFoundCard = true;
|
||||
}
|
||||
|
||||
numberOfCardsThatCanFit += 1;
|
||||
} else if (hasFoundCard) {
|
||||
// this card is off the screen to the left
|
||||
// we know how many cards can fit on a screen
|
||||
// find the new target and scroll
|
||||
const firstCardOffsetLeft = cards[0].offsetLeft;
|
||||
const cardIndexToScrollTo = i + 1 - numberOfCardsThatCanFit;
|
||||
const newFirstCard = cards[cardIndexToScrollTo];
|
||||
|
||||
let scrollTarget;
|
||||
if (newFirstCard) {
|
||||
scrollTarget = newFirstCard.offsetLeft;
|
||||
} else {
|
||||
// more cards can fit on the screen than are currently hidden
|
||||
// just scroll to the first card
|
||||
scrollTarget = cards[0].offsetLeft;
|
||||
}
|
||||
|
||||
scrollTarget -= firstCardOffsetLeft; // to play nice with the margins
|
||||
|
||||
this.handleScroll(cardRow, scrollTarget);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { category, names, categoryLink } = this.props;
|
||||
const { canScrollNext, canScrollPrevious } = this.state;
|
||||
|
||||
// The lint was throwing an error saying we should use <button> instead of <a>
|
||||
// We are using buttons, needs more exploration
|
||||
return (
|
||||
<div className="card-row">
|
||||
<div className="card-row__header">
|
||||
<div className="card-row__title">
|
||||
<h3>
|
||||
{categoryLink ? (
|
||||
<Button
|
||||
className="button-text no-underline"
|
||||
label={category}
|
||||
navigate="/show"
|
||||
navigateParams={{ uri: categoryLink }}
|
||||
/>
|
||||
) : (
|
||||
category
|
||||
)}
|
||||
</h3>
|
||||
{category &&
|
||||
category.match(/^community/i) && (
|
||||
<ToolTip
|
||||
label={__("What's this?")}
|
||||
body={__(
|
||||
'Community Content is a public space where anyone can share content with the rest of the LBRY community. Bid on the names "one," "two," "three," "four" and "five" to put your content here!'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
inverse
|
||||
circle
|
||||
disabled={!canScrollPrevious}
|
||||
onClick={this.handleScrollPrevious}
|
||||
icon="chevron-left"
|
||||
/>
|
||||
<Button
|
||||
inverse
|
||||
circle
|
||||
disabled={!canScrollNext}
|
||||
onClick={this.handleScrollNext}
|
||||
icon="chevron-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={ref => {
|
||||
this.rowItems = ref;
|
||||
}}
|
||||
className="card-row__scrollhouse"
|
||||
>
|
||||
{names &&
|
||||
names.map(name => (
|
||||
<FileCard key={name} displayStyle="card" uri={lbryuri.normalize(name)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CategoryList;
|
43
src/renderer/component/common/icon.jsx
Normal file
43
src/renderer/component/common/icon.jsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import * as icons from 'constants/icons';
|
||||
|
||||
type Props = {
|
||||
icon: string,
|
||||
fixed?: boolean,
|
||||
padded?: boolean,
|
||||
};
|
||||
|
||||
class Icon extends React.PureComponent<Props> {
|
||||
getIconTitle() {
|
||||
const { icon } = this.props;
|
||||
|
||||
switch (icon) {
|
||||
case icons.FEATURED:
|
||||
return __('Watch this and earn rewards.');
|
||||
case icons.LOCAL:
|
||||
return __('You have a copy of this file.');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { icon, fixed, padded } = this.props;
|
||||
const iconClassName = icon.startsWith('icon-') ? icon : `icon-${icon}`;
|
||||
const title = this.getIconTitle();
|
||||
|
||||
const spanClassName = classnames(
|
||||
{
|
||||
'icon--fixed-width': fixed,
|
||||
'icon--padded': padded,
|
||||
},
|
||||
iconClassName
|
||||
);
|
||||
|
||||
return <span className={spanClassName} title={title} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default Icon;
|
57
src/renderer/component/common/tooltip.jsx
Normal file
57
src/renderer/component/common/tooltip.jsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Icon from 'component/common/icon';
|
||||
import Button from 'component/link';
|
||||
|
||||
type Props = {
|
||||
body: string,
|
||||
label: string,
|
||||
};
|
||||
|
||||
type State = {
|
||||
showTooltip: boolean,
|
||||
};
|
||||
|
||||
class ToolTip extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showTooltip: false,
|
||||
};
|
||||
|
||||
(this: any).handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
const { showTooltip } = this.state;
|
||||
|
||||
if (!showTooltip) {
|
||||
document.addEventListener('click', this.handleClick);
|
||||
} else {
|
||||
document.removeEventListener('click', this.handleClick);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
showTooltip: !showTooltip,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, body } = this.props;
|
||||
const { showTooltip } = this.state;
|
||||
|
||||
return (
|
||||
<span className="tooltip">
|
||||
<Button fakeLink className="help tooltip__link" onClick={this.handleClick}>
|
||||
{label}
|
||||
{showTooltip && <Icon icon="times" fixed />}
|
||||
</Button>
|
||||
<div className={classnames('tooltip__body', { hidden: !showTooltip })}>{body}</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ToolTip;
|
|
@ -1,111 +1,100 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import lbryuri from 'lbryuri.js';
|
||||
import lbryuri from 'lbryuri';
|
||||
import CardMedia from 'component/cardMedia';
|
||||
import Link from 'component/link';
|
||||
import { TruncatedText } from 'component/common';
|
||||
import Icon from 'component/icon';
|
||||
import Icon from 'component/common/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';
|
||||
|
||||
class FileCard extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hovered: false,
|
||||
};
|
||||
}
|
||||
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<Props> {
|
||||
componentWillMount() {
|
||||
this.resolve(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
this.resolve(nextProps);
|
||||
}
|
||||
|
||||
resolve(props) {
|
||||
resolve = (props: 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 obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||
const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw;
|
||||
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
|
||||
|
||||
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.');
|
||||
}
|
||||
// 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.');
|
||||
// }
|
||||
|
||||
// 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
|
||||
className={`card card--small card--link ${obscureNsfw ? 'card--obscured ' : ''}`}
|
||||
onMouseEnter={this.handleMouseOver.bind(this)}
|
||||
onMouseLeave={this.handleMouseOut.bind(this)}
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
onClick={() => navigate('/show', { uri })}
|
||||
className={classnames('card card--small card__link', {
|
||||
'card--obscured': shouldObscureNsfw,
|
||||
})}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
{obscureNsfw && this.state.hovered && <NsfwOverlay />}
|
||||
{obscureNsfw && <NsfwOverlay />}
|
||||
</section>
|
||||
);
|
||||
/* eslint-enable jsx-a11y/click-events-have-key-events */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import * as icons from 'constants/icons';
|
||||
import lbryuri from 'lbryuri.js';
|
||||
|
@ -5,7 +6,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/icon';
|
||||
import Icon from 'component/common/icon';
|
||||
|
||||
class FileTile extends React.PureComponent {
|
||||
static SHOW_EMPTY_PUBLISH = 'publish';
|
||||
|
@ -133,3 +134,4 @@ class FileTile extends React.PureComponent {
|
|||
}
|
||||
|
||||
export default FileTile;
|
||||
/* eslint-enable */
|
||||
|
|
|
@ -1,8 +1,20 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Link from 'component/link';
|
||||
import Button from 'component/link';
|
||||
import WunderBar from 'component/wunderbar';
|
||||
|
||||
export const Header = props => {
|
||||
type Props = {
|
||||
balance: string,
|
||||
back: any => void,
|
||||
forward: any => void,
|
||||
isBackDisabled: boolean,
|
||||
isForwardDisabled: boolean,
|
||||
isUpgradeAvailable: boolean,
|
||||
navigate: any => void,
|
||||
downloadUpgrade: any => void,
|
||||
};
|
||||
|
||||
export const Header = (props: Props) => {
|
||||
const {
|
||||
balance,
|
||||
back,
|
||||
|
@ -15,85 +27,57 @@ export const Header = props => {
|
|||
} = props;
|
||||
return (
|
||||
<header id="header">
|
||||
<div className="header__item">
|
||||
<Link
|
||||
<div className="header__actions-left">
|
||||
<Button
|
||||
alt
|
||||
circle
|
||||
onClick={back}
|
||||
disabled={isBackDisabled}
|
||||
button="alt button--flat"
|
||||
icon="icon-arrow-left"
|
||||
title={__('Back')}
|
||||
icon="arrow-left"
|
||||
description={__('Navigate back')}
|
||||
/>
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link
|
||||
|
||||
<Button
|
||||
alt
|
||||
circle
|
||||
onClick={forward}
|
||||
disabled={isForwardDisabled}
|
||||
button="alt button--flat"
|
||||
icon="icon-arrow-right"
|
||||
title={__('Forward')}
|
||||
icon="arrow-right"
|
||||
description={__('Navigate forward')}
|
||||
/>
|
||||
|
||||
<Button alt onClick={() => navigate('/discover')} icon="home" description={__('Home')} />
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link
|
||||
onClick={() => navigate('/discover')}
|
||||
button="alt button--flat"
|
||||
icon="icon-home"
|
||||
title={__('Discover Content')}
|
||||
/>
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link
|
||||
onClick={() => navigate('/subscriptions')}
|
||||
button="alt button--flat"
|
||||
icon="icon-at"
|
||||
title={__('My Subscriptions')}
|
||||
/>
|
||||
</div>
|
||||
<div className="header__item header__item--wunderbar">
|
||||
<WunderBar />
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link
|
||||
|
||||
<WunderBar />
|
||||
|
||||
<div className="header__actions-right">
|
||||
<Button
|
||||
inverse
|
||||
onClick={() => navigate('/wallet')}
|
||||
button="text"
|
||||
className="no-underline"
|
||||
icon="icon-bank"
|
||||
label={balance}
|
||||
icon="user"
|
||||
label={isUpgradeAvailable ? `${balance} LBC` : `You have ${balance} LBC`}
|
||||
title={__('Wallet')}
|
||||
/>
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link
|
||||
|
||||
<Button
|
||||
onClick={() => navigate('/publish')}
|
||||
button="primary button--flat"
|
||||
icon="icon-upload"
|
||||
label={__('Publish')}
|
||||
icon="cloud-upload"
|
||||
label={isUpgradeAvailable ? '' : __('Publish')}
|
||||
/>
|
||||
|
||||
<Button alt onClick={() => navigate('/settings')} icon="gear" title={__('Settings')} />
|
||||
|
||||
<Button alt onClick={() => navigate('/help')} icon="question" title={__('Help')} />
|
||||
{isUpgradeAvailable && (
|
||||
<Button
|
||||
onClick={() => downloadUpgrade()}
|
||||
icon="arrow-up"
|
||||
label={__('Upgrade App')}
|
||||
title={__('Upgrade app')}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Icon from './view';
|
||||
|
||||
export default connect(null, null)(Icon);
|
|
@ -1,50 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as icons from 'constants/icons';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default class Icon extends React.PureComponent {
|
||||
static propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
fixed: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
fixed: false,
|
||||
};
|
||||
|
||||
getIconClass() {
|
||||
const { icon } = this.props;
|
||||
|
||||
return icon.startsWith('icon-') ? icon : `icon-${icon}`;
|
||||
}
|
||||
|
||||
getIconTitle() {
|
||||
switch (this.props.icon) {
|
||||
case icons.FEATURED:
|
||||
return __('Watch this and earn rewards.');
|
||||
case icons.LOCAL:
|
||||
return __('You have a copy of this file.');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { icon, fixed, className, leftPad } = this.props;
|
||||
const iconClass = this.getIconClass();
|
||||
const title = this.getIconTitle();
|
||||
|
||||
const spanClassName = classnames(
|
||||
'icon',
|
||||
iconClass,
|
||||
{
|
||||
'icon-fixed-width': fixed,
|
||||
'icon--left-pad': leftPad,
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
return <span className={spanClassName} title={title} />;
|
||||
}
|
||||
}
|
|
@ -1,60 +1,99 @@
|
|||
import React from 'react';
|
||||
import Icon from 'component/icon';
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import Icon from 'component/common/icon';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const Link = props => {
|
||||
type Props = {
|
||||
onClick: ?(any) => any,
|
||||
href: ?string,
|
||||
title: ?string,
|
||||
label: ?string,
|
||||
icon: ?string,
|
||||
iconRight: ?string,
|
||||
disabled: ?boolean,
|
||||
children: ?React.Node,
|
||||
navigate: ?string,
|
||||
// TODO: these (nav) should be a reusable type
|
||||
doNavigate: (string, ?any) => void,
|
||||
navigateParams: any,
|
||||
className: ?string,
|
||||
inverse: ?boolean,
|
||||
circle: ?boolean,
|
||||
alt: ?boolean,
|
||||
flat: ?boolean,
|
||||
fakeLink: ?boolean,
|
||||
description: ?string,
|
||||
};
|
||||
|
||||
const Button = (props: Props) => {
|
||||
const {
|
||||
onClick,
|
||||
href,
|
||||
title,
|
||||
style,
|
||||
label,
|
||||
icon,
|
||||
iconRight,
|
||||
button,
|
||||
disabled,
|
||||
children,
|
||||
navigate,
|
||||
navigateParams,
|
||||
doNavigate,
|
||||
className,
|
||||
span,
|
||||
inverse,
|
||||
alt,
|
||||
circle,
|
||||
flat,
|
||||
fakeLink,
|
||||
description,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
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 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 onClick =
|
||||
!props.onClick && navigate
|
||||
const extendedOnClick =
|
||||
!onClick && navigate
|
||||
? event => {
|
||||
event.stopPropagation();
|
||||
doNavigate(navigate, navigateParams || {});
|
||||
}
|
||||
: props.onClick;
|
||||
: onClick;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
|
||||
const linkProps = {
|
||||
className: combinedClassName,
|
||||
href: href || 'javascript:;',
|
||||
title,
|
||||
onClick,
|
||||
style,
|
||||
};
|
||||
|
||||
return span ? <span {...linkProps}>{content}</span> : <a {...linkProps}>{content}</a>;
|
||||
return href ? (
|
||||
<a className={combinedClassName} href={href} title={title}>
|
||||
{content}
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
aria-label={description || title}
|
||||
className={combinedClassName}
|
||||
onClick={extendedOnClick}
|
||||
disabled={disabled}
|
||||
{...otherProps}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Link;
|
||||
export default Button;
|
||||
|
|
9
src/renderer/component/page/index.js
Normal file
9
src/renderer/component/page/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectPageTitle } from 'redux/selectors/navigation';
|
||||
import Page from './view';
|
||||
|
||||
const select = state => ({
|
||||
title: selectPageTitle(state),
|
||||
});
|
||||
|
||||
export default connect(select, null)(Page);
|
26
src/renderer/component/page/view.jsx
Normal file
26
src/renderer/component/page/view.jsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { BusyMessage } from 'component/common';
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
title: ?string,
|
||||
noPadding: ?boolean,
|
||||
isLoading: ?boolean,
|
||||
};
|
||||
|
||||
const Page = (props: Props) => {
|
||||
const { children, title, noPadding, isLoading } = props;
|
||||
return (
|
||||
<main id="main-content">
|
||||
<div className="page__header">
|
||||
{title && <h1 className="page__title">{title}</h1>}
|
||||
{isLoading && <BusyMessage message={__('Fetching content')} />}
|
||||
</div>
|
||||
<div className={classnames('main', { 'main--no-padding': noPadding })}>{children}</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
|
@ -1,54 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export class ToolTip extends React.PureComponent {
|
||||
static propTypes = {
|
||||
body: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showTooltip: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
this.setState({
|
||||
showTooltip: !this.state.showTooltip,
|
||||
});
|
||||
}
|
||||
|
||||
handleTooltipMouseOut() {
|
||||
this.setState({
|
||||
showTooltip: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span className={`tooltip ${this.props.className || ''}`}>
|
||||
<a
|
||||
className="tooltip__link"
|
||||
onClick={() => {
|
||||
this.handleClick();
|
||||
}}
|
||||
>
|
||||
{this.props.label}
|
||||
</a>
|
||||
<div
|
||||
className={`tooltip__body ${this.state.showTooltip ? '' : ' hidden'}`}
|
||||
onMouseOut={() => {
|
||||
this.handleTooltipMouseOut();
|
||||
}}
|
||||
>
|
||||
{this.props.body}
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ToolTip;
|
|
@ -1,35 +1,46 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from 'component/icon';
|
||||
import Link from 'component/link';
|
||||
import { Icon } from 'component/common';
|
||||
import Button from 'component/link';
|
||||
import lbryuri from 'lbryuri';
|
||||
import classnames from 'classnames';
|
||||
|
||||
class UriIndicator extends React.PureComponent {
|
||||
type Props = {
|
||||
isResolvingUri: boolean,
|
||||
resolveUri: string => void,
|
||||
claim: {
|
||||
channel_name: string,
|
||||
has_signature: boolean,
|
||||
signature_is_valid: boolean,
|
||||
value: {
|
||||
publisherSignature: { certificateId: string },
|
||||
},
|
||||
},
|
||||
uri: string,
|
||||
link: ?boolean,
|
||||
};
|
||||
|
||||
class UriIndicator extends React.PureComponent<Props> {
|
||||
componentWillMount() {
|
||||
this.resolve(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
this.resolve(nextProps);
|
||||
}
|
||||
|
||||
resolve(props) {
|
||||
resolve = (props: Props) => {
|
||||
const { isResolvingUri, resolveUri, claim, uri } = props;
|
||||
|
||||
if (!isResolvingUri && claim === undefined && uri) {
|
||||
resolveUri(uri);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { claim, link, uri, isResolvingUri, smallCard, span } = this.props;
|
||||
|
||||
if (isResolvingUri && !claim) {
|
||||
return <span className="empty">Validating...</span>;
|
||||
}
|
||||
|
||||
const { claim, link, isResolvingUri } = this.props;
|
||||
if (!claim) {
|
||||
return <span className="empty">Unused</span>;
|
||||
return <span className="empty">{isResolvingUri ? 'Validating...' : 'Unused'}</span>;
|
||||
}
|
||||
|
||||
const {
|
||||
|
@ -38,14 +49,17 @@ class UriIndicator extends React.PureComponent {
|
|||
signature_is_valid: signatureIsValid,
|
||||
value,
|
||||
} = claim;
|
||||
|
||||
const channelClaimId =
|
||||
value && value.publisherSignature && value.publisherSignature.certificateId;
|
||||
|
||||
if (!hasSignature || !channelName) {
|
||||
return <span className="empty">Anonymous</span>;
|
||||
return <span>Anonymous</span>;
|
||||
}
|
||||
|
||||
let icon, channelLink, modifier;
|
||||
let icon;
|
||||
let channelLink;
|
||||
let modifier;
|
||||
|
||||
if (signatureIsValid) {
|
||||
modifier = 'valid';
|
||||
|
@ -59,7 +73,6 @@ class UriIndicator extends React.PureComponent {
|
|||
<span>
|
||||
<span
|
||||
className={classnames('channel-name', {
|
||||
'channel-name--small': smallCard,
|
||||
'button-text no-underline': link,
|
||||
})}
|
||||
>
|
||||
|
@ -81,14 +94,9 @@ class UriIndicator extends React.PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
navigate="/show"
|
||||
navigateParams={{ uri: channelLink }}
|
||||
className="no-underline"
|
||||
span={span}
|
||||
>
|
||||
<Button navigate="/show" navigateParams={{ uri: channelLink }} fakeLink>
|
||||
{inner}
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import lbryuri from 'lbryuri.js';
|
||||
import { selectWunderBarAddress, selectWunderBarIcon } from 'redux/selectors/search';
|
||||
import lbryuri from 'lbryuri';
|
||||
import { selectState as selectSearch, selectWunderBarAddress } 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);
|
||||
|
|
601
src/renderer/component/wunderbar/internal/autocomplete.jsx
Normal file
601
src/renderer/component/wunderbar/internal/autocomplete.jsx
Normal file
|
@ -0,0 +1,601 @@
|
|||
/*
|
||||
This is taken from https://github.com/reactjs/react-autocomplete
|
||||
|
||||
We aren't using that component because (for now) there is no way to autohightlight
|
||||
the first item if it isn't an exact match from what is in the search bar.
|
||||
|
||||
Our use case is:
|
||||
value in search bar: "hello"
|
||||
first suggestion: "lbry://hello"
|
||||
|
||||
I changed the function maybeAutoCompleteText to check if the suggestion contains
|
||||
the search query anywhere, instead of the suggestion starting with it
|
||||
|
||||
https://github.com/reactjs/react-autocomplete/issues/239
|
||||
*/
|
||||
/* eslint-disable */
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const { findDOMNode } = require('react-dom');
|
||||
const scrollIntoView = require('dom-scroll-into-view');
|
||||
|
||||
const IMPERATIVE_API = [
|
||||
'blur',
|
||||
'checkValidity',
|
||||
'click',
|
||||
'focus',
|
||||
'select',
|
||||
'setCustomValidity',
|
||||
'setSelectionRange',
|
||||
'setRangeText',
|
||||
];
|
||||
|
||||
function getScrollOffset() {
|
||||
return {
|
||||
x:
|
||||
window.pageXOffset !== undefined
|
||||
? window.pageXOffset
|
||||
: (document.documentElement || document.body.parentNode || document.body).scrollLeft,
|
||||
y:
|
||||
window.pageYOffset !== undefined
|
||||
? window.pageYOffset
|
||||
: (document.documentElement || document.body.parentNode || document.body).scrollTop,
|
||||
};
|
||||
}
|
||||
|
||||
export default class Autocomplete extends React.Component {
|
||||
static propTypes = {
|
||||
/**
|
||||
* The items to display in the dropdown menu
|
||||
*/
|
||||
items: PropTypes.array.isRequired,
|
||||
/**
|
||||
* The value to display in the input field
|
||||
*/
|
||||
value: PropTypes.any,
|
||||
/**
|
||||
* Arguments: `event: Event, value: String`
|
||||
*
|
||||
* Invoked every time the user changes the input's value.
|
||||
*/
|
||||
onChange: PropTypes.func,
|
||||
/**
|
||||
* Arguments: `value: String, item: Any`
|
||||
*
|
||||
* Invoked when the user selects an item from the dropdown menu.
|
||||
*/
|
||||
onSelect: PropTypes.func,
|
||||
/**
|
||||
* Arguments: `item: Any, value: String`
|
||||
*
|
||||
* Invoked for each entry in `items` and its return value is used to
|
||||
* determine whether or not it should be displayed in the dropdown menu.
|
||||
* By default all items are always rendered.
|
||||
*/
|
||||
shouldItemRender: PropTypes.func,
|
||||
/**
|
||||
* Arguments: `itemA: Any, itemB: Any, value: String`
|
||||
*
|
||||
* The function which is used to sort `items` before display.
|
||||
*/
|
||||
sortItems: PropTypes.func,
|
||||
/**
|
||||
* Arguments: `item: Any`
|
||||
*
|
||||
* Used to read the display value from each entry in `items`.
|
||||
*/
|
||||
getItemValue: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Arguments: `item: Any, isHighlighted: Boolean, styles: Object`
|
||||
*
|
||||
* Invoked for each entry in `items` that also passes `shouldItemRender` to
|
||||
* generate the render tree for each item in the dropdown menu. `styles` is
|
||||
* an optional set of styles that can be applied to improve the look/feel
|
||||
* of the items in the dropdown menu.
|
||||
*/
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Arguments: `items: Array<Any>, value: String, styles: Object`
|
||||
*
|
||||
* Invoked to generate the render tree for the dropdown menu. Ensure the
|
||||
* returned tree includes every entry in `items` or else the highlight order
|
||||
* and keyboard navigation logic will break. `styles` will contain
|
||||
* { top, left, minWidth } which are the coordinates of the top-left corner
|
||||
* and the width of the dropdown menu.
|
||||
*/
|
||||
renderMenu: PropTypes.func,
|
||||
/**
|
||||
* Styles that are applied to the dropdown menu in the default `renderMenu`
|
||||
* implementation. If you override `renderMenu` and you want to use
|
||||
* `menuStyle` you must manually apply them (`this.props.menuStyle`).
|
||||
*/
|
||||
menuStyle: PropTypes.object,
|
||||
/**
|
||||
* Arguments: `props: Object`
|
||||
*
|
||||
* Invoked to generate the input element. The `props` argument is the result
|
||||
* of merging `props.inputProps` with a selection of props that are required
|
||||
* both for functionality and accessibility. At the very least you need to
|
||||
* apply `props.ref` and all `props.on<event>` event handlers. Failing to do
|
||||
* this will cause `Autocomplete` to behave unexpectedly.
|
||||
*/
|
||||
renderInput: PropTypes.func,
|
||||
/**
|
||||
* Props passed to `props.renderInput`. By default these props will be
|
||||
* applied to the `<input />` element rendered by `Autocomplete`, unless you
|
||||
* have specified a custom value for `props.renderInput`. Any properties
|
||||
* supported by `HTMLInputElement` can be specified, apart from the
|
||||
* following which are set by `Autocomplete`: value, autoComplete, role,
|
||||
* aria-autocomplete. `inputProps` is commonly used for (but not limited to)
|
||||
* placeholder, event handlers (onFocus, onBlur, etc.), autoFocus, etc..
|
||||
*/
|
||||
inputProps: PropTypes.object,
|
||||
/**
|
||||
* Props that are applied to the element which wraps the `<input />` and
|
||||
* dropdown menu elements rendered by `Autocomplete`.
|
||||
*/
|
||||
wrapperProps: PropTypes.object,
|
||||
/**
|
||||
* This is a shorthand for `wrapperProps={{ style: <your styles> }}`.
|
||||
* Note that `wrapperStyle` is applied before `wrapperProps`, so the latter
|
||||
* will win if it contains a `style` entry.
|
||||
*/
|
||||
wrapperStyle: PropTypes.object,
|
||||
/**
|
||||
* Whether or not to automatically highlight the top match in the dropdown
|
||||
* menu.
|
||||
*/
|
||||
autoHighlight: PropTypes.bool,
|
||||
/**
|
||||
* Whether or not to automatically select the highlighted item when the
|
||||
* `<input>` loses focus.
|
||||
*/
|
||||
selectOnBlur: PropTypes.bool,
|
||||
/**
|
||||
* Arguments: `isOpen: Boolean`
|
||||
*
|
||||
* Invoked every time the dropdown menu's visibility changes (i.e. every
|
||||
* time it is displayed/hidden).
|
||||
*/
|
||||
onMenuVisibilityChange: PropTypes.func,
|
||||
/**
|
||||
* Used to override the internal logic which displays/hides the dropdown
|
||||
* menu. This is useful if you want to force a certain state based on your
|
||||
* UX/business logic. Use it together with `onMenuVisibilityChange` for
|
||||
* fine-grained control over the dropdown menu dynamics.
|
||||
*/
|
||||
open: PropTypes.bool,
|
||||
debug: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
value: '',
|
||||
wrapperProps: {},
|
||||
wrapperStyle: {
|
||||
display: 'inline-block',
|
||||
},
|
||||
inputProps: {},
|
||||
renderInput(props) {
|
||||
return <input {...props} />;
|
||||
},
|
||||
onChange() {},
|
||||
onSelect() {},
|
||||
renderMenu(items, value, style) {
|
||||
return <div style={{ ...style, ...this.menuStyle }} children={items} />;
|
||||
},
|
||||
menuStyle: {
|
||||
borderRadius: '3px',
|
||||
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
padding: '2px 0',
|
||||
fontSize: '90%',
|
||||
position: 'fixed',
|
||||
overflow: 'auto',
|
||||
maxHeight: '50%', // TODO: don't cheat, let it flow to the bottom,
|
||||
},
|
||||
autoHighlight: true,
|
||||
selectOnBlur: false,
|
||||
onMenuVisibilityChange() {},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
highlightedIndex: null,
|
||||
};
|
||||
this._debugStates = [];
|
||||
this.ensureHighlightedIndex = this.ensureHighlightedIndex.bind(this);
|
||||
this.exposeAPI = this.exposeAPI.bind(this);
|
||||
this.handleInputFocus = this.handleInputFocus.bind(this);
|
||||
this.handleInputBlur = this.handleInputBlur.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleInputClick = this.handleInputClick.bind(this);
|
||||
this.maybeAutoCompleteText = this.maybeAutoCompleteText.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
// this.refs is frozen, so we need to assign a new object to it
|
||||
this.refs = {};
|
||||
this._ignoreBlur = false;
|
||||
this._ignoreFocus = false;
|
||||
this._scrollOffset = null;
|
||||
this._scrollTimer = null;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this._scrollTimer);
|
||||
this._scrollTimer = null;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.state.highlightedIndex !== null) {
|
||||
this.setState(this.ensureHighlightedIndex);
|
||||
}
|
||||
if (
|
||||
nextProps.autoHighlight &&
|
||||
(this.props.value !== nextProps.value || this.state.highlightedIndex === null)
|
||||
) {
|
||||
this.setState(this.maybeAutoCompleteText);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.isOpen()) {
|
||||
this.setMenuPositions();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (
|
||||
(this.state.isOpen && !prevState.isOpen) ||
|
||||
('open' in this.props && this.props.open && !prevProps.open)
|
||||
)
|
||||
this.setMenuPositions();
|
||||
|
||||
this.maybeScrollItemIntoView();
|
||||
if (prevState.isOpen !== this.state.isOpen) {
|
||||
this.props.onMenuVisibilityChange(this.state.isOpen);
|
||||
}
|
||||
}
|
||||
|
||||
exposeAPI(el) {
|
||||
this.refs.input = el;
|
||||
IMPERATIVE_API.forEach(ev => (this[ev] = el && el[ev] && el[ev].bind(el)));
|
||||
}
|
||||
|
||||
maybeScrollItemIntoView() {
|
||||
if (this.isOpen() && this.state.highlightedIndex !== null) {
|
||||
const itemNode = this.refs[`item-${this.state.highlightedIndex}`];
|
||||
const menuNode = this.refs.menu;
|
||||
scrollIntoView(findDOMNode(itemNode), findDOMNode(menuNode), {
|
||||
onlyScrollIfNeeded: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown(event) {
|
||||
if (Autocomplete.keyDownHandlers[event.key])
|
||||
Autocomplete.keyDownHandlers[event.key].call(this, event);
|
||||
else if (!this.isOpen()) {
|
||||
this.setState({
|
||||
isOpen: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
this.props.onChange(event, event.target.value);
|
||||
}
|
||||
|
||||
static keyDownHandlers = {
|
||||
ArrowDown(event) {
|
||||
event.preventDefault();
|
||||
const itemsLength = this.getFilteredItems(this.props).length;
|
||||
if (!itemsLength) return;
|
||||
const { highlightedIndex } = this.state;
|
||||
const index =
|
||||
highlightedIndex === null || highlightedIndex === itemsLength - 1
|
||||
? 0
|
||||
: highlightedIndex + 1;
|
||||
this.setState({
|
||||
highlightedIndex: index,
|
||||
isOpen: true,
|
||||
});
|
||||
},
|
||||
|
||||
ArrowUp(event) {
|
||||
event.preventDefault();
|
||||
const itemsLength = this.getFilteredItems(this.props).length;
|
||||
if (!itemsLength) return;
|
||||
const { highlightedIndex } = this.state;
|
||||
const index =
|
||||
highlightedIndex === 0 || highlightedIndex === null
|
||||
? itemsLength - 1
|
||||
: highlightedIndex - 1;
|
||||
this.setState({
|
||||
highlightedIndex: index,
|
||||
isOpen: true,
|
||||
});
|
||||
},
|
||||
|
||||
Enter(event) {
|
||||
// Key code 229 is used for selecting items from character selectors (Pinyin, Kana, etc)
|
||||
if (event.keyCode !== 13) return;
|
||||
if (!this.isOpen()) {
|
||||
// menu is closed so there is no selection to accept -> do nothing
|
||||
} else if (this.state.highlightedIndex == null) {
|
||||
// input has focus but no menu item is selected + enter is hit -> close the menu, highlight whatever's in input
|
||||
this.setState(
|
||||
{
|
||||
isOpen: false,
|
||||
},
|
||||
() => {
|
||||
this.refs.input.select();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// text entered + menu item has been highlighted + enter is hit -> update value to that of selected menu item, close the menu
|
||||
event.preventDefault();
|
||||
const item = this.getFilteredItems(this.props)[this.state.highlightedIndex];
|
||||
const value = this.props.getItemValue(item);
|
||||
this.setState(
|
||||
{
|
||||
isOpen: false,
|
||||
highlightedIndex: null,
|
||||
},
|
||||
() => {
|
||||
// this.refs.input.focus() // TODO: file issue
|
||||
this.refs.input.setSelectionRange(value.length, value.length);
|
||||
this.props.onSelect(value, item);
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
Escape() {
|
||||
// In case the user is currently hovering over the menu
|
||||
this.setIgnoreBlur(false);
|
||||
this.setState({
|
||||
highlightedIndex: null,
|
||||
isOpen: false,
|
||||
});
|
||||
},
|
||||
|
||||
Tab() {
|
||||
// In case the user is currently hovering over the menu
|
||||
this.setIgnoreBlur(false);
|
||||
},
|
||||
};
|
||||
|
||||
getFilteredItems(props) {
|
||||
let items = props.items;
|
||||
|
||||
if (props.shouldItemRender) {
|
||||
items = items.filter(item => props.shouldItemRender(item, props.value));
|
||||
}
|
||||
|
||||
if (props.sortItems) {
|
||||
items.sort((a, b) => props.sortItems(a, b, props.value));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
maybeAutoCompleteText(state, props) {
|
||||
const { highlightedIndex } = state;
|
||||
const { value, getItemValue } = props;
|
||||
const index = highlightedIndex === null ? 0 : highlightedIndex;
|
||||
const matchedItem = this.getFilteredItems(props)[index];
|
||||
if (value !== '' && matchedItem) {
|
||||
const itemValue = getItemValue(matchedItem);
|
||||
const itemValueDoesMatch =
|
||||
itemValue.toLowerCase().indexOf(
|
||||
value.toLowerCase()
|
||||
// below line is the the only thing that is changed from the real component
|
||||
) !== -1;
|
||||
if (itemValueDoesMatch) {
|
||||
return { highlightedIndex: index };
|
||||
}
|
||||
}
|
||||
return { highlightedIndex: null };
|
||||
}
|
||||
|
||||
ensureHighlightedIndex(state, props) {
|
||||
if (state.highlightedIndex >= this.getFilteredItems(props).length) {
|
||||
return { highlightedIndex: null };
|
||||
}
|
||||
}
|
||||
|
||||
setMenuPositions() {
|
||||
const node = this.refs.input;
|
||||
const rect = node.getBoundingClientRect();
|
||||
const computedStyle = global.window.getComputedStyle(node);
|
||||
const marginBottom = parseInt(computedStyle.marginBottom, 10) || 0;
|
||||
const marginLeft = parseInt(computedStyle.marginLeft, 10) || 0;
|
||||
const marginRight = parseInt(computedStyle.marginRight, 10) || 0;
|
||||
this.setState({
|
||||
menuTop: rect.bottom + marginBottom,
|
||||
menuLeft: rect.left + marginLeft,
|
||||
menuWidth: rect.width + marginLeft + marginRight,
|
||||
});
|
||||
}
|
||||
|
||||
highlightItemFromMouse(index) {
|
||||
this.setState({ highlightedIndex: index });
|
||||
}
|
||||
|
||||
selectItemFromMouse(item) {
|
||||
const value = this.props.getItemValue(item);
|
||||
// The menu will de-render before a mouseLeave event
|
||||
// happens. Clear the flag to release control over focus
|
||||
this.setIgnoreBlur(false);
|
||||
this.setState(
|
||||
{
|
||||
isOpen: false,
|
||||
highlightedIndex: null,
|
||||
},
|
||||
() => {
|
||||
this.props.onSelect(value, item);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setIgnoreBlur(ignore) {
|
||||
this._ignoreBlur = ignore;
|
||||
}
|
||||
|
||||
renderMenu() {
|
||||
const items = this.getFilteredItems(this.props).map((item, index) => {
|
||||
const element = this.props.renderItem(item, this.state.highlightedIndex === index, {
|
||||
cursor: 'default',
|
||||
});
|
||||
return React.cloneElement(element, {
|
||||
onMouseEnter: () => this.highlightItemFromMouse(index),
|
||||
onClick: () => this.selectItemFromMouse(item),
|
||||
ref: e => (this.refs[`item-${index}`] = e),
|
||||
});
|
||||
});
|
||||
const style = {
|
||||
left: this.state.menuLeft,
|
||||
top: this.state.menuTop,
|
||||
minWidth: this.state.menuWidth,
|
||||
};
|
||||
const menu = this.props.renderMenu(items, this.props.value, style);
|
||||
return React.cloneElement(menu, {
|
||||
ref: e => (this.refs.menu = e),
|
||||
// Ignore blur to prevent menu from de-rendering before we can process click
|
||||
onMouseEnter: () => this.setIgnoreBlur(true),
|
||||
onMouseLeave: () => this.setIgnoreBlur(false),
|
||||
});
|
||||
}
|
||||
|
||||
handleInputBlur(event) {
|
||||
if (this._ignoreBlur) {
|
||||
this._ignoreFocus = true;
|
||||
this._scrollOffset = getScrollOffset();
|
||||
this.refs.input.focus();
|
||||
return;
|
||||
}
|
||||
let setStateCallback;
|
||||
const { highlightedIndex } = this.state;
|
||||
if (this.props.selectOnBlur && highlightedIndex !== null) {
|
||||
const items = this.getFilteredItems(this.props);
|
||||
const item = items[highlightedIndex];
|
||||
const value = this.props.getItemValue(item);
|
||||
setStateCallback = () => this.props.onSelect(value, item);
|
||||
}
|
||||
this.setState(
|
||||
{
|
||||
isOpen: false,
|
||||
highlightedIndex: null,
|
||||
},
|
||||
setStateCallback
|
||||
);
|
||||
const { onBlur } = this.props.inputProps;
|
||||
if (onBlur) {
|
||||
onBlur(event);
|
||||
}
|
||||
}
|
||||
|
||||
handleInputFocus(event) {
|
||||
if (this._ignoreFocus) {
|
||||
this._ignoreFocus = false;
|
||||
const { x, y } = this._scrollOffset;
|
||||
this._scrollOffset = null;
|
||||
// Focus will cause the browser to scroll the <input> into view.
|
||||
// This can cause the mouse coords to change, which in turn
|
||||
// could cause a new highlight to happen, cancelling the click
|
||||
// event (when selecting with the mouse)
|
||||
window.scrollTo(x, y);
|
||||
// Some browsers wait until all focus event handlers have been
|
||||
// processed before scrolling the <input> into view, so let's
|
||||
// scroll again on the next tick to ensure we're back to where
|
||||
// the user was before focus was lost. We could do the deferred
|
||||
// scroll only, but that causes a jarring split second jump in
|
||||
// some browsers that scroll before the focus event handlers
|
||||
// are triggered.
|
||||
clearTimeout(this._scrollTimer);
|
||||
this._scrollTimer = setTimeout(() => {
|
||||
this._scrollTimer = null;
|
||||
window.scrollTo(x, y);
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
this.setState({ isOpen: true });
|
||||
const { onFocus } = this.props.inputProps;
|
||||
if (onFocus) {
|
||||
onFocus(event);
|
||||
}
|
||||
}
|
||||
|
||||
isInputFocused() {
|
||||
const el = this.refs.input;
|
||||
return el.ownerDocument && el === el.ownerDocument.activeElement;
|
||||
}
|
||||
|
||||
handleInputClick() {
|
||||
// Input will not be focused if it's disabled
|
||||
if (this.isInputFocused() && !this.isOpen()) this.setState({ isOpen: true });
|
||||
}
|
||||
|
||||
composeEventHandlers(internal, external) {
|
||||
return external
|
||||
? e => {
|
||||
internal(e);
|
||||
external(e);
|
||||
}
|
||||
: internal;
|
||||
}
|
||||
|
||||
isOpen() {
|
||||
return 'open' in this.props ? this.props.open : this.state.isOpen;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.debug) {
|
||||
// you don't like it, you love it
|
||||
this._debugStates.push({
|
||||
id: this._debugStates.length,
|
||||
state: this.state,
|
||||
});
|
||||
}
|
||||
|
||||
const { inputProps, items } = this.props;
|
||||
|
||||
const open = this.isOpen();
|
||||
return (
|
||||
<div style={{ ...this.props.wrapperStyle }} {...this.props.wrapperProps}>
|
||||
{this.props.renderInput({
|
||||
...inputProps,
|
||||
role: 'combobox',
|
||||
'aria-autocomplete': 'list',
|
||||
'aria-expanded': open,
|
||||
autoComplete: 'off',
|
||||
ref: this.exposeAPI,
|
||||
onFocus: this.handleInputFocus,
|
||||
onBlur: this.handleInputBlur,
|
||||
onChange: this.handleChange,
|
||||
onKeyDown: this.composeEventHandlers(this.handleKeyDown, inputProps.onKeyDown),
|
||||
onClick: this.composeEventHandlers(this.handleInputClick, inputProps.onClick),
|
||||
value: this.props.value,
|
||||
})}
|
||||
{open && !!items.length && this.renderMenu()}
|
||||
{this.props.debug && (
|
||||
<pre style={{ marginLeft: 300 }}>
|
||||
{JSON.stringify(
|
||||
this._debugStates.slice(
|
||||
Math.max(0, this._debugStates.length - 5),
|
||||
this._debugStates.length
|
||||
),
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,166 +1,116 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import lbryuri from 'lbryuri.js';
|
||||
import Icon from 'component/icon';
|
||||
import { parseQueryParams } from 'util/query_params';
|
||||
import lbryuri from 'lbryuri';
|
||||
import classnames from 'classnames';
|
||||
import Autocomplete from './internal/autocomplete';
|
||||
|
||||
class WunderBar extends React.PureComponent {
|
||||
static TYPING_TIMEOUT = 800;
|
||||
type Props = {
|
||||
updateSearchQuery: string => void,
|
||||
getSearchSuggestions: string => void,
|
||||
onSearch: string => void,
|
||||
onSubmit: string => void,
|
||||
searchQuery: ?string,
|
||||
isActive: boolean,
|
||||
address: ?string,
|
||||
suggestions: Array<string>,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.userTypingTimer) {
|
||||
clearTimeout(this._userTypingTimer);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
onChange(event) {
|
||||
if (this._userTypingTimer) {
|
||||
clearTimeout(this._userTypingTimer);
|
||||
handleSubmit(value: string) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ address: event.target.value });
|
||||
const { onSubmit, onSearch } = this.props;
|
||||
|
||||
this._isSearchDispatchPending = true;
|
||||
// if they choose the "search for {value}" in the suggestions
|
||||
// it will contain the {query}?search
|
||||
const choseDoSuggestedSearch = value.endsWith('?search');
|
||||
|
||||
const searchQuery = event.target.value;
|
||||
|
||||
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 });
|
||||
let searchValue = value;
|
||||
if (choseDoSuggestedSearch) {
|
||||
searchValue = value.slice(0, -7); // trim off ?search
|
||||
}
|
||||
}
|
||||
|
||||
onFocus() {
|
||||
this._stateBeforeSearch = this.state;
|
||||
const newState = {
|
||||
icon: 'icon-search',
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
this._focusPending = true;
|
||||
// below is hacking, improved when we have proper routing
|
||||
if (!this.state.address.startsWith('lbry://') && this.state.icon !== 'icon-search') {
|
||||
// onFocus, if they are not on an exact URL or a search page, clear the bar
|
||||
newState.address = '';
|
||||
if (this.input) {
|
||||
this.input.blur();
|
||||
}
|
||||
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);
|
||||
}
|
||||
try {
|
||||
const uri = lbryuri.normalize(value);
|
||||
onSubmit(uri);
|
||||
} catch (e) {
|
||||
// search query isn't a valid uri
|
||||
onSearch(searchValue);
|
||||
}
|
||||
}
|
||||
|
||||
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={`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
|
||||
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>
|
||||
);
|
||||
|
|
|
@ -91,6 +91,10 @@ export const FILE_DELETE = 'FILE_DELETE';
|
|||
export const SEARCH_STARTED = 'SEARCH_STARTED';
|
||||
export const SEARCH_COMPLETED = 'SEARCH_COMPLETED';
|
||||
export const SEARCH_CANCELLED = 'SEARCH_CANCELLED';
|
||||
export const UPDATE_SEARCH_QUERY = 'UPDATE_SEARCH_QUERY';
|
||||
export const GET_SEARCH_SUGGESTIONS_START = 'GET_SEARCH_SUGGESTIONS_START';
|
||||
export const GET_SEARCH_SUGGESTIONS_SUCCESS = 'GET_SEARCH_SUGGESTIONS_SUCCESS';
|
||||
export const GET_SEARCH_SUGGESTIONS_FAIL = 'GET_SEARCH_SUGGESTIONS_FAIL';
|
||||
|
||||
// Settings
|
||||
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);
|
||||
console.log(error); // eslint-disable-line no-console
|
||||
}
|
||||
if (verification.token && verification.recaptcha) {
|
||||
app.store.dispatch(doConditionalAuthNavigate(newSession));
|
||||
|
@ -112,10 +112,10 @@ const init = () => {
|
|||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<div>
|
||||
<React.Fragment>
|
||||
<App />
|
||||
<SnackBar />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</Provider>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import lbryuri from 'lbryuri';
|
||||
import { BusyMessage } from 'component/common';
|
||||
|
@ -5,6 +6,7 @@ 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() {
|
||||
|
@ -70,7 +72,7 @@ class ChannelPage extends React.PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Page>
|
||||
<section className="card">
|
||||
<div className="card__inner">
|
||||
<div className="card__title-identity">
|
||||
|
@ -107,9 +109,10 @@ class ChannelPage extends React.PureComponent {
|
|||
containerClassName="pagination"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelPage;
|
||||
/* eslint-enable */
|
||||
|
|
|
@ -1,259 +1,37 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
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';
|
||||
import Page from 'component/page';
|
||||
import CategoryList from 'component/common/category-list';
|
||||
|
||||
// This should be in a separate file
|
||||
export class FeaturedCategory extends React.PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
type Props = {
|
||||
fetchFeaturedUris: () => void,
|
||||
fetchingFeaturedUris: boolean,
|
||||
featuredUris: {},
|
||||
};
|
||||
|
||||
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 {
|
||||
class DiscoverPage extends React.PureComponent<Props> {
|
||||
componentWillMount() {
|
||||
this.props.fetchFeaturedUris();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { featuredUris, fetchingFeaturedUris } = this.props;
|
||||
const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length,
|
||||
failedToLoad = !fetchingFeaturedUris && !hasContent;
|
||||
const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length;
|
||||
const failedToLoad = !fetchingFeaturedUris && !hasContent;
|
||||
|
||||
return (
|
||||
<main
|
||||
className={classnames('main main--no-margin', {
|
||||
reloading: hasContent && fetchingFeaturedUris,
|
||||
})}
|
||||
>
|
||||
<SubHeader fullWidth smallMargin />
|
||||
{!hasContent && fetchingFeaturedUris && <BusyMessage message={__('Fetching content')} />}
|
||||
<Page noPadding isLoading={!hasContent && fetchingFeaturedUris}>
|
||||
{hasContent &&
|
||||
Object.keys(featuredUris).map(
|
||||
category =>
|
||||
featuredUris[category].length ? (
|
||||
<FeaturedCategory
|
||||
key={category}
|
||||
category={category}
|
||||
names={featuredUris[category]}
|
||||
/>
|
||||
<CategoryList key={category} category={category} names={featuredUris[category]} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
)}
|
||||
{failedToLoad && <div className="empty">{__('Failed to load landing content.')}</div>}
|
||||
</main>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import lbry from 'lbry';
|
||||
import lbryuri from 'lbryuri';
|
||||
|
@ -6,12 +7,13 @@ import { Thumbnail } from 'component/common';
|
|||
import FilePrice from 'component/filePrice';
|
||||
import FileDetails from 'component/fileDetails';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import Icon from 'component/icon';
|
||||
import Icon from 'component/common/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() {
|
||||
|
@ -69,49 +71,52 @@ class FilePage extends React.PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
|
||||
<FileDetails uri={uri} />
|
||||
</div>
|
||||
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
|
||||
<FileDetails uri={uri} />
|
||||
</div>
|
||||
)}
|
||||
{tab === 'tip' && <WalletSendTip claim_id={claim.claim_id} uri={uri} />}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{tab === 'tip' && <WalletSendTip claim_id={claim.claim_id} uri={uri} />}
|
||||
</div>
|
||||
</section>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FilePage;
|
||||
/* eslint-enable */
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import lbryuri from 'lbryuri';
|
||||
import FileTile from 'component/fileTile';
|
||||
import FileListSearch from 'component/fileListSearch';
|
||||
import { ToolTip } from 'component/tooltip.js';
|
||||
import ToolTip from 'component/common/tooltip';
|
||||
import Page from 'component/page';
|
||||
|
||||
class SearchPage extends React.PureComponent {
|
||||
type Props = {
|
||||
query: ?string,
|
||||
};
|
||||
|
||||
class SearchPage extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { query } = this.props;
|
||||
|
||||
return (
|
||||
<main className="main--single-column">
|
||||
<Page>
|
||||
{lbryuri.isValid(query) ? (
|
||||
<section className="section-spaced">
|
||||
<h3 className="card-row__header">
|
||||
|
@ -36,7 +42,7 @@ class SearchPage extends React.PureComponent {
|
|||
</h3>
|
||||
<FileListSearch query={query} />
|
||||
</section>
|
||||
</main>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
/* 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';
|
||||
|
||||
class ShowPage extends React.PureComponent {
|
||||
type Props = {
|
||||
isResolvingUri: boolean,
|
||||
resolveUri: string => void,
|
||||
uri: string,
|
||||
claim: { name: string },
|
||||
};
|
||||
|
||||
class ShowPage extends React.PureComponent<Props> {
|
||||
componentWillMount() {
|
||||
const { isResolvingUri, resolveUri, uri } = this.props;
|
||||
|
||||
if (!isResolvingUri) resolveUri(uri);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
const { isResolvingUri, resolveUri, claim, uri } = nextProps;
|
||||
|
||||
if (!isResolvingUri && claim === undefined && uri) {
|
||||
|
@ -47,8 +54,9 @@ class ShowPage extends React.PureComponent {
|
|||
innerContent = <FilePage uri={uri} />;
|
||||
}
|
||||
|
||||
return <main className="main--single-column">{innerContent}</main>;
|
||||
return innerContent;
|
||||
}
|
||||
}
|
||||
|
||||
export default ShowPage;
|
||||
/* eslint-enable */
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import SubHeader from 'component/subHeader';
|
||||
import { BusyMessage } from 'component/common.js';
|
||||
import { FeaturedCategory } from 'page/discover/view';
|
||||
import { BusyMessage } from 'component/common';
|
||||
import CategoryList from 'component/common/category-list';
|
||||
import type { Subscription } from 'redux/reducers/subscriptions';
|
||||
|
||||
type SavedSubscriptions = Array<Subscription>;
|
||||
|
@ -83,7 +83,7 @@ export default class extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
return (
|
||||
<FeaturedCategory
|
||||
<CategoryList
|
||||
key={subscription.channelName}
|
||||
categoryLink={subscription.uri}
|
||||
category={subscription.channelName}
|
||||
|
|
|
@ -5,63 +5,120 @@ 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 handleResponse = response =>
|
||||
response.status === 200
|
||||
? Promise.resolve(response.json())
|
||||
: Promise.reject(new Error(response.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, '');
|
||||
|
||||
if (!query) {
|
||||
dispatch({
|
||||
type: ACTIONS.SEARCH_CANCELLED,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const query = rawQuery.replace(/^lbry:\/\//i, '');
|
||||
|
||||
if (!query) {
|
||||
dispatch({
|
||||
type: ACTIONS.SEARCH_STARTED,
|
||||
data: { query },
|
||||
type: ACTIONS.SEARCH_CANCELLED,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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 = [];
|
||||
dispatch({
|
||||
type: ACTIONS.SEARCH_STARTED,
|
||||
data: { query },
|
||||
});
|
||||
|
||||
data.forEach(result => {
|
||||
const uri = Lbryuri.build({
|
||||
name: result.name,
|
||||
claimId: result.claimId,
|
||||
});
|
||||
actions.push(doResolveUri(uri));
|
||||
uris.push(uri);
|
||||
});
|
||||
if (page !== 'search') {
|
||||
dispatch(doNavigate('search', { query }));
|
||||
} else {
|
||||
fetch(`https://lighthouse.lbry.io/search?s=${query}`)
|
||||
.then(handleResponse)
|
||||
.then(data => {
|
||||
const uris = [];
|
||||
const actions = [];
|
||||
|
||||
actions.push({
|
||||
type: ACTIONS.SEARCH_COMPLETED,
|
||||
data: {
|
||||
query,
|
||||
uris,
|
||||
},
|
||||
});
|
||||
dispatch(batchActions(...actions));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch({
|
||||
type: ACTIONS.SEARCH_CANCELLED,
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSearchQuery = searchQuery => ({
|
||||
type: ACTIONS.UPDATE_SEARCH_QUERY,
|
||||
data: { searchQuery },
|
||||
});
|
||||
|
||||
export const getSearchSuggestions = value => dispatch => {
|
||||
dispatch({ type: ACTIONS.GET_SEARCH_SUGGESTIONS_START });
|
||||
if (!value) {
|
||||
dispatch({
|
||||
type: ACTIONS.GET_SEARCH_SUGGESTIONS_SUCCESS,
|
||||
data: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// This should probably be more robust
|
||||
let searchValue = value;
|
||||
if (searchValue.startsWith('lbry://')) {
|
||||
searchValue = searchValue.slice(7);
|
||||
}
|
||||
|
||||
// need to handle spaces in the query?
|
||||
fetch(`https://lighthouse.lbry.io/autocomplete?s=${searchValue}`)
|
||||
.then(handleResponse)
|
||||
.then(suggestions => {
|
||||
const formattedSuggestions = suggestions.slice(0, 5).map(suggestion => ({
|
||||
label: suggestion,
|
||||
value: suggestion,
|
||||
}));
|
||||
|
||||
// Should we add lbry://{query} as the first result?
|
||||
// If it's not a valid uri, then add a "search for {query}" result
|
||||
const searchLabel = `Search for "${value}"`;
|
||||
try {
|
||||
const uri = Lbryuri.normalize(value);
|
||||
formattedSuggestions.unshift(
|
||||
{ label: uri, value: uri },
|
||||
{ label: searchLabel, value: `${value}?search` }
|
||||
);
|
||||
} catch (e) {
|
||||
if (value) {
|
||||
formattedSuggestions.unshift({ label: searchLabel, value });
|
||||
}
|
||||
}
|
||||
|
||||
return dispatch({
|
||||
type: ACTIONS.GET_SEARCH_SUGGESTIONS_SUCCESS,
|
||||
data: formattedSuggestions,
|
||||
});
|
||||
})
|
||||
.catch(err =>
|
||||
dispatch({
|
||||
type: ACTIONS.GET_SEARCH_SUGGESTIONS_FAIL,
|
||||
data: err,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,32 +1,75 @@
|
|||
// @flow
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import { handleActions } from 'util/redux-utils';
|
||||
|
||||
const reducers = {};
|
||||
const defaultState = {
|
||||
type SearchState = {
|
||||
isActive: boolean,
|
||||
searchQuery: string,
|
||||
searchingForSuggestions: boolean,
|
||||
suggestions: Array<string>,
|
||||
urisByQuery: {},
|
||||
searching: false,
|
||||
};
|
||||
|
||||
reducers[ACTIONS.SEARCH_STARTED] = state =>
|
||||
Object.assign({}, state, {
|
||||
searching: true,
|
||||
});
|
||||
|
||||
reducers[ACTIONS.SEARCH_COMPLETED] = (state, action) => {
|
||||
const { query, uris } = action.data;
|
||||
|
||||
return Object.assign({}, state, {
|
||||
searching: false,
|
||||
urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }),
|
||||
});
|
||||
const defaultState = {
|
||||
isActive: false,
|
||||
searchQuery: '', // needs to be an empty string for input focusing
|
||||
searchingForSuggestions: false,
|
||||
suggestions: [],
|
||||
urisByQuery: {},
|
||||
};
|
||||
|
||||
reducers[ACTIONS.SEARCH_CANCELLED] = state =>
|
||||
Object.assign({}, state, {
|
||||
searching: false,
|
||||
});
|
||||
export default handleActions(
|
||||
{
|
||||
[ACTIONS.SEARCH_STARTED]: (state: SearchState): SearchState => ({
|
||||
...state,
|
||||
searching: true,
|
||||
}),
|
||||
[ACTIONS.SEARCH_COMPLETED]: (state: SearchState, action): SearchState => {
|
||||
const { query, uris } = action.data;
|
||||
|
||||
export default function reducer(state = defaultState, action) {
|
||||
const handler = reducers[action.type];
|
||||
if (handler) return handler(state, action);
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
searching: false,
|
||||
urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }),
|
||||
};
|
||||
},
|
||||
|
||||
[ACTIONS.SEARCH_CANCELLED]: (state: SearchState): SearchState => ({
|
||||
...state,
|
||||
searching: false,
|
||||
}),
|
||||
|
||||
[ACTIONS.UPDATE_SEARCH_QUERY]: (state: SearchState, action): SearchState => ({
|
||||
...state,
|
||||
searchQuery: action.data.searchQuery,
|
||||
suggestions: [],
|
||||
isActive: true,
|
||||
}),
|
||||
|
||||
[ACTIONS.GET_SEARCH_SUGGESTIONS_START]: (state: SearchState): SearchState => ({
|
||||
...state,
|
||||
searchingForSuggestions: true,
|
||||
suggestions: [],
|
||||
}),
|
||||
[ACTIONS.GET_SEARCH_SUGGESTIONS_SUCCESS]: (state: SearchState, action): SearchState => ({
|
||||
...state,
|
||||
searchingForSuggestions: false,
|
||||
suggestions: action.data,
|
||||
}),
|
||||
[ACTIONS.GET_SEARCH_SUGGESTIONS_FAIL]: (state: SearchState): SearchState => ({
|
||||
...state,
|
||||
searchingForSuggestions: false,
|
||||
// error, TODO: figure this out on the search page
|
||||
}),
|
||||
|
||||
// clear the searchQuery on back/forward
|
||||
// it may be populated by the page title for search/file pages
|
||||
// if going home, it should be blank
|
||||
[ACTIONS.HISTORY_NAVIGATE]: (state: SearchState): SearchState => ({
|
||||
...state,
|
||||
searchQuery: '',
|
||||
isActive: false,
|
||||
}),
|
||||
},
|
||||
defaultState
|
||||
);
|
||||
|
|
|
@ -68,30 +68,6 @@ 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"
|
||||
|
@ -100,21 +76,14 @@ 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 page[0].toUpperCase() + (page.length > 0 ? page.substr(1) : '');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -28,52 +28,15 @@ export const selectWunderBarAddress = createSelector(
|
|||
selectCurrentPage,
|
||||
selectPageTitle,
|
||||
selectSearchQuery,
|
||||
(page, title, query) => (page !== 'search' ? title : query || title)
|
||||
);
|
||||
|
||||
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';
|
||||
(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;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,4 +1,54 @@
|
|||
@import url(https://fonts.googleapis.com/css?family=Roboto:400,400i,500,500i,700);
|
||||
// Generic html styles used accross the App
|
||||
// component specific styling should go in the component scss file
|
||||
|
||||
@font-face {
|
||||
font-family: 'Metropolis';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
text-rendering: optimizeLegibility;
|
||||
src: url('../../../static/font/metropolis/Metropolis-Medium.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Metropolis';
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
text-rendering: optimizeLegibility;
|
||||
src: url('../../../static/font/metropolis/Metropolis-SemiBold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
// @font-face {
|
||||
// font-family: 'Metropolis';
|
||||
// font-weight: 700;
|
||||
// font-style: normal;
|
||||
// text-rendering: optimizeLegibility;
|
||||
// src: url('../../../static/font/metropolis/Metropolis-SemiBold.woff2') format('woff2');
|
||||
// }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Metropolis';
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
text-rendering: optimizeLegibility;
|
||||
src: url('../../../static/font/metropolis/Metropolis-ExtraBold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
// TODO: use this
|
||||
// @font-face {
|
||||
// font-family: 'Metropolis';
|
||||
// font-weight: normal;
|
||||
// font-style: italic;
|
||||
// text-rendering: optimizeLegibility;
|
||||
// src: url('../../../static/font/metropolis/Metropolis-MediumItalic.woff2') format('woff2');
|
||||
// }
|
||||
//
|
||||
// @font-face {
|
||||
// font-family: 'Metropolis';
|
||||
// font-weight: lighter;
|
||||
// font-style: normal;
|
||||
// text-rendering: optimizeLegibility;
|
||||
// src: url('../../../static/font/metropolis/Metropolis-Light.woff2') format('woff2');
|
||||
// }
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
|
@ -7,84 +57,20 @@ html {
|
|||
|
||||
body {
|
||||
color: var(--text-color);
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-family: 'Metropolis', sans-serif;
|
||||
line-height: var(--font-line-height);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
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;
|
||||
}
|
||||
|
@ -100,11 +86,13 @@ h4 {
|
|||
h5 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
sup,
|
||||
sub {
|
||||
vertical-align: baseline;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.4em;
|
||||
}
|
||||
|
@ -117,11 +105,66 @@ code {
|
|||
background-color: var(--color-bg-alt);
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0.8em;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
// Why is this needed?
|
||||
button {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
#window {
|
||||
height: 100%;
|
||||
overflow: hidden; // so the scrollbar will not extend into the header
|
||||
}
|
||||
|
||||
#main-content {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
// don't use {bottom/top} here
|
||||
// they cause flashes of un-rendered content when scrolling
|
||||
margin-top: var(--header-height);
|
||||
padding-bottom: var(--header-height); // fix this scrollbar extends beyond screen
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 0 $spacing-vertical * 2/3;
|
||||
}
|
||||
|
||||
.main--no-padding {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.page__header {
|
||||
padding: $spacing-vertical * 2/3;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.page__title {
|
||||
font-weight: 800;
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
/* Custom text selection */
|
||||
*::selection {
|
||||
background: var(--text-selection-bg);
|
||||
color: var(--text-selection-color);
|
||||
}
|
||||
|
||||
.credit-amount--indicator {
|
||||
font-weight: 500;
|
||||
color: var(--color-money);
|
||||
}
|
||||
|
||||
.credit-amount--fee {
|
||||
font-size: 0.9em;
|
||||
color: var(--color-meta-light);
|
||||
}
|
||||
|
||||
.credit-amount--bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
|
@ -192,7 +235,3 @@ p {
|
|||
section.section-spaced {
|
||||
margin-bottom: $spacing-vertical;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,16 @@
|
|||
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,6 +6,8 @@ $width-page-constrained: 800px;
|
|||
$text-color: #000;
|
||||
|
||||
:root {
|
||||
--spacing-vertical: 24px;
|
||||
|
||||
/* Colors */
|
||||
--color-brand: #155b4a;
|
||||
--color-primary: #155b4a;
|
||||
|
@ -21,7 +23,8 @@ $text-color: #000;
|
|||
--color-download: rgba(0, 0, 0, 0.75);
|
||||
--color-canvas: #f5f5f5;
|
||||
--color-bg: #ffffff;
|
||||
--color-bg-alt: #d9d9d9;
|
||||
--color-bg-alt: #f6f6f6;
|
||||
--color-placeholder: #ececec;
|
||||
|
||||
/* Misc */
|
||||
--content-max-width: 1000px;
|
||||
|
@ -34,7 +37,7 @@ $text-color: #000;
|
|||
--font-size-subtext-multiple: 0.82;
|
||||
|
||||
/* Shadows */
|
||||
--box-shadow-layer: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
--box-shadow-layer: 0px 1px 3px 0px rgba(0, 0, 0, 0.2);
|
||||
--box-shadow-focus: 2px 4px 4px 0 rgba(0, 0, 0, 0.14), 2px 5px 3px -2px rgba(0, 0, 0, 0.2),
|
||||
2px 3px 7px 0 rgba(0, 0, 0, 0.12);
|
||||
|
||||
|
@ -50,9 +53,6 @@ $text-color: #000;
|
|||
--text-selection-bg: rgba(saturate(lighten(#155b4a, 20%), 20%), 1); // temp color
|
||||
--text-selection-color: #fff;
|
||||
|
||||
/* Window */
|
||||
--window-bg: var(--color-canvas);
|
||||
|
||||
/* Form */
|
||||
--form-label-color: rgba(0, 0, 0, 0.54);
|
||||
|
||||
|
@ -80,21 +80,23 @@ $text-color: #000;
|
|||
--select-bg: var(--color-bg-alt);
|
||||
--select-color: var(--text-color);
|
||||
|
||||
//TODO: determine proper button variables;
|
||||
/* Button */
|
||||
--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;
|
||||
--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;
|
||||
|
||||
/* Header */
|
||||
--header-bg: var(--color-bg);
|
||||
--header-color: #666;
|
||||
--header-active-color: rgba(0, 0, 0, 0.85);
|
||||
--header-height: $spacing-vertical * 2.5;
|
||||
--header-height: 65px;
|
||||
--header-button-bg: transparent; //var(--button-bg);
|
||||
--header-button-hover-bg: rgba(100, 100, 100, 0.15);
|
||||
|
||||
|
@ -142,7 +144,6 @@ $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,89 +1,100 @@
|
|||
@import '../mixin/link.scss';
|
||||
// Styles for the <Button /> component
|
||||
// may be a button or a html element
|
||||
|
||||
$button-focus-shift: 12%;
|
||||
/*
|
||||
|
||||
.button-set-item {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
TODO:
|
||||
Determine [disabled] or .disabled
|
||||
|
||||
+ .button-set-item {
|
||||
margin-left: var(--button-intra-margin);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
.button-block,
|
||||
.faux-button-block {
|
||||
display: inline-block;
|
||||
height: var(--button-height);
|
||||
line-height: var(--button-height);
|
||||
button {
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
border: 0 none;
|
||||
text-align: center;
|
||||
border-radius: var(--button-radius);
|
||||
text-transform: uppercase;
|
||||
.icon {
|
||||
top: 0em;
|
||||
}
|
||||
.icon:first-child {
|
||||
padding-right: 5px;
|
||||
}
|
||||
.icon:last-child {
|
||||
padding-left: 5px;
|
||||
}
|
||||
.icon:only-child {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
.button-block {
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
transition: background var(--animation-duration) var(--animation-style);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.button__content {
|
||||
margin: 0 var(--button-padding);
|
||||
display: flex;
|
||||
.link-label {
|
||||
text-decoration: none !important;
|
||||
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-primary {
|
||||
color: var(--button-primary-color);
|
||||
background-color: var(--button-primary-bg);
|
||||
box-shadow: var(--box-shadow-layer);
|
||||
button.btn.btn--alt {
|
||||
color: var(--btn-alt-color);
|
||||
background-color: #efefef;
|
||||
|
||||
&:focus {
|
||||
//color: var(--button-primary-active-color);
|
||||
//background-color:color: var(--button-primary-active-bg);
|
||||
//box-shadow: $box-shadow-focus;
|
||||
&:hover {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #cdcdcd;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--color-help);
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.button-alt {
|
||||
background-color: var(--button-bg);
|
||||
|
||||
button.btn.btn--circle {
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover:not([disabled]) {
|
||||
border-radius: var(--btn-radius);
|
||||
}
|
||||
}
|
||||
|
||||
button.btn.btn--inverse {
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
color: var(--btn-primary-bg);
|
||||
}
|
||||
|
||||
button.btn--link {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: inherit;
|
||||
font-size: 0.9em;
|
||||
color: var(--btn-primary-bg); // this should be a different color
|
||||
|
||||
&:hover {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
}
|
||||
|
||||
.btn__label {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.btn--with-help + .button__help {
|
||||
position: absolute;
|
||||
// display: none;
|
||||
transition: 0s display;
|
||||
z-index: 1;
|
||||
width: 120px;
|
||||
// top: -5px;
|
||||
left: 105%;
|
||||
border-radius: 5px;
|
||||
background-color: var(--color-placeholder);
|
||||
box-shadow: var(--box-shadow-layer);
|
||||
}
|
||||
|
||||
.button-text {
|
||||
@include text-link();
|
||||
display: inline-block;
|
||||
|
||||
.button__content {
|
||||
margin: 0 var(--text-link-padding);
|
||||
}
|
||||
}
|
||||
.button-text-help {
|
||||
@include text-link(var(--text-help-color));
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.button--flat {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.button--submit {
|
||||
font-family: inherit;
|
||||
line-height: 0;
|
||||
.btn--with-help:hover + .button__help {
|
||||
display: block;
|
||||
transition-delay: 1s;
|
||||
}
|
||||
|
|
|
@ -2,194 +2,19 @@
|
|||
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;
|
||||
|
||||
//below added to prevent scrollbar on long titles when show page loads, would prefer a cleaner CSS solution
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.card--obscured {
|
||||
position: relative;
|
||||
}
|
||||
.card--obscured .card__inner {
|
||||
filter: blur(var(--nsfw-blur-intensity));
|
||||
}
|
||||
.card__title-primary,
|
||||
.card__title-identity,
|
||||
.card__content,
|
||||
.card__subtext,
|
||||
.card__actions {
|
||||
padding: 0 var(--card-padding);
|
||||
}
|
||||
|
||||
.card--small {
|
||||
.card__title-primary,
|
||||
.card__title-identity,
|
||||
.card__actions,
|
||||
.card__content,
|
||||
.card__subtext {
|
||||
padding: 0 calc(var(--card-padding) / 2);
|
||||
}
|
||||
}
|
||||
.card__title-primary {
|
||||
margin-top: var(--card-margin);
|
||||
margin-bottom: var(--card-margin);
|
||||
}
|
||||
.card__title-primary .meta {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.card__title-identity {
|
||||
margin: 16px 0;
|
||||
}
|
||||
.card__actions {
|
||||
margin-top: var(--card-margin);
|
||||
margin-bottom: var(--card-margin);
|
||||
user-select: none;
|
||||
}
|
||||
.card__actions--bottom {
|
||||
margin-top: $spacing-vertical * 1/3;
|
||||
margin-bottom: $spacing-vertical * 1/3;
|
||||
border-top: var(--divider);
|
||||
}
|
||||
.card__actions--form-submit {
|
||||
margin-top: $spacing-vertical;
|
||||
margin-bottom: var(--card-margin);
|
||||
}
|
||||
.card__action--right {
|
||||
float: right;
|
||||
}
|
||||
.card__content {
|
||||
margin-top: var(--card-margin);
|
||||
margin-bottom: var(--card-margin);
|
||||
table:not(:last-child) {
|
||||
margin-bottom: var(--card-margin);
|
||||
}
|
||||
}
|
||||
|
||||
.card__actions--only-vertical {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.card__content--extra-vertical-space {
|
||||
margin: $spacing-vertical 0;
|
||||
}
|
||||
|
||||
$font-size-subtext-multiple: 0.82;
|
||||
.card__subtext {
|
||||
color: var(--color-meta-light);
|
||||
font-size: calc(var(--font-size-subtext-multiple) * 1em);
|
||||
margin-top: $spacing-vertical * 1/3;
|
||||
margin-bottom: $spacing-vertical * 1/3;
|
||||
}
|
||||
.card__subtext--allow-newlines {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.card__subtext--two-lines {
|
||||
height: calc(
|
||||
var(--font-size) * var(--font-size-subtext-multiple) * var(--font-line-height) * 2
|
||||
); /*this is so one line text still has the proper height*/
|
||||
}
|
||||
.card-overlay {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
padding: 20px;
|
||||
background-color: var(--color-dark-overlay);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
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--placeholder {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.card--small {
|
||||
width: var(--card-small-width);
|
||||
min-height: var(--card-small-width);
|
||||
overflow-x: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
|
@ -197,126 +22,237 @@ $font-size-subtext-multiple: 0.82;
|
|||
height: calc(var(--card-small-width) * 9 / 16);
|
||||
}
|
||||
|
||||
.card--form {
|
||||
width: calc(var(--input-width) + var(--card-padding) * 2);
|
||||
.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__subtitle {
|
||||
color: var(--color-help);
|
||||
font-size: 0.85em;
|
||||
line-height: calc(var(--font-line-height) * 1 / 0.85);
|
||||
padding-top: $spacing-vertical * 1/3;
|
||||
}
|
||||
|
||||
.card--file-subtitle {
|
||||
display: flex;
|
||||
}
|
||||
// .card__title-primary .meta {
|
||||
// white-space: nowrap;
|
||||
// overflow: hidden;
|
||||
// text-overflow: ellipsis;
|
||||
// }
|
||||
//
|
||||
|
||||
// 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__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);
|
||||
// }
|
||||
//
|
||||
|
||||
.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);
|
||||
margin-right: $spacing-vertical;
|
||||
padding-top: $spacing-vertical;
|
||||
}
|
||||
|
||||
.card-row__header {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
// specific padding-left styling is needed here
|
||||
// this needs to be used on a page with noPadding
|
||||
// doing so allows the content to scroll to the edge of the screen
|
||||
padding-left: $spacing-vertical * 2/3;
|
||||
}
|
||||
|
||||
.card-row__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-row__scrollhouse {
|
||||
position: relative;
|
||||
/*hacky way to give space for hover */
|
||||
padding-right: $padding-right-card-hover-hack;
|
||||
}
|
||||
padding-top: $spacing-vertical * 2/3;
|
||||
overflow: hidden;
|
||||
|
||||
.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);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transform: scale(calc(var(--card-link-scaling) * 1.1));
|
||||
.card {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-left: $spacing-vertical * 2/3;
|
||||
}
|
||||
|
||||
.card:last-of-type {
|
||||
padding-right: $spacing-vertical * 2/3;
|
||||
}
|
||||
}
|
||||
.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
|
||||
*/
|
||||
.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;
|
||||
}
|
||||
}
|
||||
//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;
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -5,12 +5,6 @@
|
|||
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,64 +1,60 @@
|
|||
#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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
color: var(--header-color);
|
||||
background-color: var(--header-bg);
|
||||
}
|
||||
|
||||
.header__item--wunderbar {
|
||||
flex-grow: 1;
|
||||
.header__actions-left {
|
||||
display: flex;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.wunderbar {
|
||||
position: relative;
|
||||
.icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: $spacing-vertical / 2 - 4px; //hacked
|
||||
}
|
||||
.header__actions-right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.wunderbar--active .icon-search {
|
||||
color: var(--color-primary);
|
||||
.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;
|
||||
}
|
||||
|
||||
// below styles should be inside the common input styling
|
||||
// will come back to this with the redesign - sean
|
||||
.wunderbar__input {
|
||||
background: var(--search-bg);
|
||||
height: 50%;
|
||||
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);
|
||||
padding: 10px;
|
||||
background-color: #f3f3f3;
|
||||
border-radius: 10px;
|
||||
font-size: 0.9em;
|
||||
|
||||
&:focus {
|
||||
background: var(--search-active-bg);
|
||||
color: var(--search-active-color);
|
||||
box-shadow: var(--search-active-shadow);
|
||||
border-color: var(--color-primary);
|
||||
// TODO: focus style
|
||||
}
|
||||
}
|
||||
|
||||
.wunderbar__suggestion {
|
||||
padding: 5px;
|
||||
background-color: var(--header-bg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wunderbar__active-suggestion {
|
||||
background-color: #a3ffb0;
|
||||
}
|
||||
|
|
|
@ -2,10 +2,7 @@
|
|||
|
||||
.tooltip {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip__link {
|
||||
@include text-link();
|
||||
padding: 0 $spacing-vertical / 3;
|
||||
}
|
||||
|
||||
.tooltip__body {
|
||||
|
@ -17,16 +14,15 @@
|
|||
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--header .tooltip__link {
|
||||
@include text-link(#aaa);
|
||||
.tooltip__link {
|
||||
font-size: calc(var(--font-size) * 3/4);
|
||||
margin-left: var(--button-padding);
|
||||
vertical-align: middle;
|
||||
|
|
BIN
static/font/metropolis/Metropolis-BlackItalic.woff2
Executable file
BIN
static/font/metropolis/Metropolis-BlackItalic.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-Bold.woff2
Executable file
BIN
static/font/metropolis/Metropolis-Bold.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-BoldItalic.woff2
Executable file
BIN
static/font/metropolis/Metropolis-BoldItalic.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-ExtraBold.woff2
Executable file
BIN
static/font/metropolis/Metropolis-ExtraBold.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-ExtraBoldItalic.woff2
Executable file
BIN
static/font/metropolis/Metropolis-ExtraBoldItalic.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-ExtraLight.woff2
Executable file
BIN
static/font/metropolis/Metropolis-ExtraLight.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-ExtraLightItalic.woff2
Executable file
BIN
static/font/metropolis/Metropolis-ExtraLightItalic.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-Light.woff2
Executable file
BIN
static/font/metropolis/Metropolis-Light.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-LightItalic.woff2
Executable file
BIN
static/font/metropolis/Metropolis-LightItalic.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-Medium.woff2
Executable file
BIN
static/font/metropolis/Metropolis-Medium.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-MediumItalic.woff2
Executable file
BIN
static/font/metropolis/Metropolis-MediumItalic.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-Regular.woff2
Executable file
BIN
static/font/metropolis/Metropolis-Regular.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-RegularItalic.woff2
Executable file
BIN
static/font/metropolis/Metropolis-RegularItalic.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-SemiBold.woff2
Executable file
BIN
static/font/metropolis/Metropolis-SemiBold.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-SemiBoldItalic.woff2
Executable file
BIN
static/font/metropolis/Metropolis-SemiBoldItalic.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-Thin.woff2
Executable file
BIN
static/font/metropolis/Metropolis-Thin.woff2
Executable file
Binary file not shown.
BIN
static/font/metropolis/Metropolis-ThinItalic.woff2
Executable file
BIN
static/font/metropolis/Metropolis-ThinItalic.woff2
Executable file
Binary file not shown.
|
@ -2661,6 +2661,10 @@ 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