Redesign groundwork #935
66 changed files with 2089 additions and 1452 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='^component\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/component\1'
|
||||||
module.name_mapper='^page\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/page\1'
|
module.name_mapper='^page\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/page\1'
|
||||||
module.name_mapper='^lbry\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/lbry\1'
|
module.name_mapper='^lbry\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/lbry\1'
|
||||||
|
module.name_mapper='^modal\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/modal\1'
|
||||||
|
|
||||||
[strict]
|
[strict]
|
||||||
|
|
3
flow-typed/react-modal.js
vendored
Normal file
3
flow-typed/react-modal.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
declare module 'react-modal' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
|
@ -32,6 +32,7 @@
|
||||||
"amplitude-js": "^4.0.0",
|
"amplitude-js": "^4.0.0",
|
||||||
"bluebird": "^3.5.1",
|
"bluebird": "^3.5.1",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
|
"dom-scroll-into-view": "^1.2.1",
|
||||||
"electron-dl": "^1.6.0",
|
"electron-dl": "^1.6.0",
|
||||||
"formik": "^0.10.4",
|
"formik": "^0.10.4",
|
||||||
"from2": "^2.3.0",
|
"from2": "^2.3.0",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Router from 'component/router/index';
|
import Router from 'component/router/index';
|
||||||
import Header from 'component/header';
|
import Header from 'component/header';
|
||||||
|
@ -6,61 +7,83 @@ import ModalRouter from 'modal/modalRouter';
|
||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
import throttle from 'util/throttle';
|
import throttle from 'util/throttle';
|
||||||
|
|
||||||
class App extends React.PureComponent {
|
type Props = {
|
||||||
|
alertError: (string | {}) => void,
|
||||||
|
recordScroll: number => void,
|
||||||
|
currentStackIndex: number,
|
||||||
|
currentPageAttributes: { path: string, scrollY: number },
|
||||||
|
pageTitle: ?string,
|
||||||
|
};
|
||||||
|
|
||||||
|
class App extends React.PureComponent<Props> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.mainContent = undefined;
|
this.mainContent = undefined;
|
||||||
|
(this: any).scrollListener = this.scrollListener.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
const { alertError } = this.props;
|
const { alertError } = this.props;
|
||||||
|
|
||||||
document.addEventListener('unhandledError', event => {
|
// TODO: create type for this object
|
||||||
|
// it lives in jsonrpc.js
|
||||||
|
document.addEventListener('unhandledError', (event: any) => {
|
||||||
alertError(event.detail);
|
alertError(event.detail);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { recordScroll } = this.props;
|
|
||||||
const mainContent = document.getElementById('main-content');
|
const mainContent = document.getElementById('main-content');
|
||||||
this.mainContent = mainContent;
|
this.mainContent = mainContent;
|
||||||
|
|
||||||
const scrollListener = () => recordScroll(this.mainContent.scrollTop);
|
if (this.mainContent) {
|
||||||
|
this.mainContent.addEventListener('scroll', throttle(this.scrollListener, 750));
|
||||||
this.mainContent.addEventListener('scroll', throttle(scrollListener, 750));
|
}
|
||||||
|
|
||||||
ReactModal.setAppElement('#window'); // fuck this
|
ReactModal.setAppElement('#window'); // fuck this
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillReceiveProps(props: Props) {
|
||||||
this.mainContent.removeEventListener('scroll', this.scrollListener);
|
const { pageTitle } = props;
|
||||||
|
this.setTitleFromProps(pageTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
this.setTitleFromProps(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { currentStackIndex: prevStackIndex } = prevProps;
|
const { currentStackIndex: prevStackIndex } = prevProps;
|
||||||
const { currentStackIndex, currentPageAttributes } = this.props;
|
const { currentStackIndex, currentPageAttributes } = this.props;
|
||||||
|
|
||||||
if (currentStackIndex !== prevStackIndex) {
|
if (this.mainContent && currentStackIndex !== prevStackIndex) {
|
||||||
this.mainContent.scrollTop = currentPageAttributes.scrollY || 0;
|
this.mainContent.scrollTop = currentPageAttributes.scrollY || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTitleFromProps(props) {
|
componentWillUnmount() {
|
||||||
window.document.title = props.pageTitle || 'LBRY';
|
if (this.mainContent) {
|
||||||
|
// having issues with this
|
||||||
|
// $FlowFixMe
|
||||||
|
this.mainContent.removeEventListener('scroll');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTitleFromProps = (title: ?string) => {
|
||||||
|
window.document.title = title || 'LBRY';
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollListener() {
|
||||||
|
const { recordScroll } = this.props;
|
||||||
|
if (this.mainContent) {
|
||||||
|
recordScroll(this.mainContent.scrollTop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mainContent: ?HTMLElement;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div id="window">
|
<div id="window">
|
||||||
<Theme />
|
<Theme />
|
||||||
<Header />
|
<Header />
|
||||||
<div id="main-content">
|
|
||||||
<Router />
|
<Router />
|
||||||
</div>
|
|
||||||
<ModalRouter />
|
<ModalRouter />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,48 +1,18 @@
|
||||||
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
class CardMedia extends React.PureComponent {
|
type Props = {
|
||||||
static AUTO_THUMB_CLASSES = [
|
thumbnail: ?string, // externally sourced image
|
||||||
'purple',
|
};
|
||||||
'red',
|
|
||||||
'pink',
|
|
||||||
'indigo',
|
|
||||||
'blue',
|
|
||||||
'light-blue',
|
|
||||||
'cyan',
|
|
||||||
'teal',
|
|
||||||
'green',
|
|
||||||
'yellow',
|
|
||||||
'orange',
|
|
||||||
];
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
this.setState({
|
|
||||||
autoThumbClass:
|
|
||||||
CardMedia.AUTO_THUMB_CLASSES[
|
|
||||||
Math.floor(Math.random() * CardMedia.AUTO_THUMB_CLASSES.length)
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
class CardMedia extends React.PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { title, thumbnail } = this.props;
|
const { thumbnail } = this.props;
|
||||||
const atClass = this.state.autoThumbClass;
|
|
||||||
|
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
return <div className="card__media" style={{ backgroundImage: `url('${thumbnail}')` }} />;
|
return <div className="card__media" style={{ backgroundImage: `url('${thumbnail}')` }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <div className="card__media card__media--autothumb">LBRY</div>;
|
||||||
<div className={`card__media card__media--autothumb ${atClass}`}>
|
|
||||||
<div className="card__autothumb__text">
|
|
||||||
{title &&
|
|
||||||
title
|
|
||||||
.replace(/\s+/g, '')
|
|
||||||
.substring(0, Math.min(title.replace(' ', '').length, 5))
|
|
||||||
.toUpperCase()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// just disabling the linter because this file shouldn't even exist
|
||||||
|
// will continue to move components over to /components/common/{comp} - sean
|
||||||
|
/* eslint-disable */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { formatCredits, formatFullPrice } from 'util/formatCredits';
|
import { formatCredits, formatFullPrice } from 'util/formatCredits';
|
||||||
|
@ -170,3 +173,4 @@ export class Thumbnail extends React.PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* eslint-enable */
|
||||||
|
|
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,101 @@
|
||||||
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import lbryuri from 'lbryuri.js';
|
import lbryuri from 'lbryuri';
|
||||||
import CardMedia from 'component/cardMedia';
|
import CardMedia from 'component/cardMedia';
|
||||||
import Link from 'component/link';
|
|
||||||
import { TruncatedText } from 'component/common';
|
import { TruncatedText } from 'component/common';
|
||||||
import Icon from 'component/icon';
|
import Icon from 'component/common/icon';
|
||||||
import FilePrice from 'component/filePrice';
|
import FilePrice from 'component/filePrice';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import NsfwOverlay from 'component/nsfwOverlay';
|
import NsfwOverlay from 'component/nsfwOverlay';
|
||||||
import TruncatedMarkdown from 'component/truncatedMarkdown';
|
|
||||||
import * as icons from 'constants/icons';
|
import * as icons from 'constants/icons';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
class FileCard extends React.PureComponent {
|
// TODO: iron these out
|
||||||
constructor(props) {
|
type Props = {
|
||||||
super(props);
|
isResolvingUri: boolean,
|
||||||
|
resolveUri: string => void,
|
||||||
this.state = {
|
uri: string,
|
||||||
hovered: false,
|
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() {
|
componentWillMount() {
|
||||||
this.resolve(this.props);
|
this.resolve(this.props);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps: Props) {
|
||||||
this.resolve(nextProps);
|
this.resolve(nextProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(props) {
|
resolve = (props: Props) => {
|
||||||
const { isResolvingUri, resolveUri, claim, uri } = props;
|
const { isResolvingUri, resolveUri, claim, uri } = props;
|
||||||
|
|
||||||
if (!isResolvingUri && claim === undefined && uri) {
|
if (!isResolvingUri && claim === undefined && uri) {
|
||||||
resolveUri(uri);
|
resolveUri(uri);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
handleMouseOver() {
|
|
||||||
this.setState({
|
|
||||||
hovered: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseOut() {
|
|
||||||
this.setState({
|
|
||||||
hovered: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
claim,
|
claim,
|
||||||
fileInfo,
|
fileInfo,
|
||||||
metadata,
|
metadata,
|
||||||
isResolvingUri,
|
|
||||||
navigate,
|
navigate,
|
||||||
rewardedContentClaimIds,
|
rewardedContentClaimIds,
|
||||||
|
obscureNsfw,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const uri = lbryuri.normalize(this.props.uri);
|
const uri = lbryuri.normalize(this.props.uri);
|
||||||
const title = metadata && metadata.title ? metadata.title : uri;
|
const title = metadata && metadata.title ? metadata.title : uri;
|
||||||
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null;
|
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null;
|
||||||
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw;
|
||||||
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
|
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
|
||||||
|
|
||||||
let description = '';
|
// Come back to this on other pages
|
||||||
if (isResolvingUri && !claim) {
|
// let description = '';
|
||||||
description = __('Loading...');
|
// if (isResolvingUri && !claim) {
|
||||||
} else if (metadata && metadata.description) {
|
// description = __('Loading...');
|
||||||
description = metadata.description;
|
// } else if (metadata && metadata.description) {
|
||||||
} else if (claim === null) {
|
// description = metadata.description;
|
||||||
description = __('This address contains no content.');
|
// } else if (claim === null) {
|
||||||
}
|
// description = __('This address contains no content.');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// We don't want to allow a click handler unless it's in focus
|
||||||
|
// I'll come back to this when I work on site-wide keyboard navigation
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={`card card--small card--link ${obscureNsfw ? 'card--obscured ' : ''}`}
|
tabIndex="0"
|
||||||
onMouseEnter={this.handleMouseOver.bind(this)}
|
role="button"
|
||||||
onMouseLeave={this.handleMouseOut.bind(this)}
|
onClick={() => navigate('/show', { uri })}
|
||||||
|
className={classnames('card card--small card__link', {
|
||||||
|
'card--obscured': shouldObscureNsfw,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<div className="card__inner">
|
<CardMedia thumbnail={thumbnail} />
|
||||||
<Link onClick={() => navigate('/show', { uri })} className="card__link">
|
|
||||||
<CardMedia title={title} thumbnail={thumbnail} />
|
|
||||||
<div className="card__title-identity">
|
<div className="card__title-identity">
|
||||||
<div className="card__title" title={title}>
|
<div className="card__title--small">
|
||||||
<TruncatedText lines={1}>{title}</TruncatedText>
|
<TruncatedText lines={3}>{title}</TruncatedText>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card__subtitle">
|
<div className="card__subtitle">
|
||||||
<span className="card__indicators card--file-subtitle">
|
<UriIndicator uri={uri} link />
|
||||||
<FilePrice uri={uri} />{' '}
|
<div className="card--file-subtitle">
|
||||||
{isRewardContent && <Icon icon={icons.FEATURED} leftPad />}{' '}
|
<FilePrice uri={uri} /> {isRewardContent && <Icon icon={icons.FEATURED} padded />}
|
||||||
{fileInfo && <Icon icon={icons.LOCAL} leftPad />}
|
{fileInfo && <Icon icon={icons.LOCAL} padded />}
|
||||||
</span>
|
|
||||||
<span className="card--file-subtitle">
|
|
||||||
<UriIndicator uri={uri} link span smallCard />
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
|
||||||
{/* Test for nizuka's design: should we remove description?
|
|
||||||
<div className="card__content card__subtext card__subtext--two-lines">
|
|
||||||
<TruncatedMarkdown lines={2}>{description}</TruncatedMarkdown>
|
|
||||||
</div>
|
</div>
|
||||||
*/}
|
{obscureNsfw && <NsfwOverlay />}
|
||||||
</div>
|
|
||||||
{obscureNsfw && this.state.hovered && <NsfwOverlay />}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
/* eslint-enable jsx-a11y/click-events-have-key-events */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BusyMessage } from 'component/common';
|
import { BusyMessage } from 'component/common';
|
||||||
import Icon from 'component/icon';
|
import Icon from 'component/common/icon';
|
||||||
import Link from 'component/link';
|
import Link from 'component/link';
|
||||||
|
|
||||||
class FileDownloadLink extends React.PureComponent {
|
class FileDownloadLink extends React.PureComponent {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as icons from 'constants/icons';
|
import * as icons from 'constants/icons';
|
||||||
import lbryuri from 'lbryuri.js';
|
import lbryuri from 'lbryuri.js';
|
||||||
|
@ -5,7 +6,7 @@ import CardMedia from 'component/cardMedia';
|
||||||
import { TruncatedText } from 'component/common.js';
|
import { TruncatedText } from 'component/common.js';
|
||||||
import FilePrice from 'component/filePrice';
|
import FilePrice from 'component/filePrice';
|
||||||
import NsfwOverlay from 'component/nsfwOverlay';
|
import NsfwOverlay from 'component/nsfwOverlay';
|
||||||
import Icon from 'component/icon';
|
import Icon from 'component/common/icon';
|
||||||
|
|
||||||
class FileTile extends React.PureComponent {
|
class FileTile extends React.PureComponent {
|
||||||
static SHOW_EMPTY_PUBLISH = 'publish';
|
static SHOW_EMPTY_PUBLISH = 'publish';
|
||||||
|
@ -133,3 +134,4 @@ class FileTile extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FileTile;
|
export default FileTile;
|
||||||
|
/* eslint-enable */
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import FormField from 'component/formField';
|
import FormField from 'component/formField';
|
||||||
import Icon from 'component/icon';
|
import Icon from 'component/common/icon';
|
||||||
|
|
||||||
let formFieldCounter = 0;
|
let formFieldCounter = 0;
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,20 @@
|
||||||
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'component/link';
|
import Button from 'component/link';
|
||||||
import WunderBar from 'component/wunderbar';
|
import WunderBar from 'component/wunderbar';
|
||||||
|
|
||||||
export const Header = props => {
|
type Props = {
|
||||||
|
balance: string,
|
||||||
|
back: any => void,
|
||||||
|
forward: any => void,
|
||||||
|
isBackDisabled: boolean,
|
||||||
|
isForwardDisabled: boolean,
|
||||||
|
isUpgradeAvailable: boolean,
|
||||||
|
navigate: any => void,
|
||||||
|
downloadUpgrade: any => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Header = (props: Props) => {
|
||||||
const {
|
const {
|
||||||
balance,
|
balance,
|
||||||
back,
|
back,
|
||||||
|
@ -15,85 +27,58 @@ export const Header = props => {
|
||||||
} = props;
|
} = props;
|
||||||
return (
|
return (
|
||||||
<header id="header">
|
<header id="header">
|
||||||
<div className="header__item">
|
<div className="header__actions-left">
|
||||||
<Link
|
<Button
|
||||||
|
alt
|
||||||
|
circle
|
||||||
onClick={back}
|
onClick={back}
|
||||||
disabled={isBackDisabled}
|
disabled={isBackDisabled}
|
||||||
button="alt button--flat"
|
icon="arrow-left"
|
||||||
icon="icon-arrow-left"
|
description={__('Navigate back')}
|
||||||
title={__('Back')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="header__item">
|
<Button
|
||||||
<Link
|
alt
|
||||||
|
circle
|
||||||
onClick={forward}
|
onClick={forward}
|
||||||
disabled={isForwardDisabled}
|
disabled={isForwardDisabled}
|
||||||
button="alt button--flat"
|
icon="arrow-right"
|
||||||
icon="icon-arrow-right"
|
description={__('Navigate forward')}
|
||||||
title={__('Forward')}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Button alt onClick={() => navigate('/discover')} icon="home" description={__('Home')} />
|
||||||
</div>
|
</div>
|
||||||
<div className="header__item">
|
|
||||||
<Link
|
|
||||||
onClick={() => navigate('/discover')}
|
|
||||||
button="alt button--flat"
|
|
||||||
icon="icon-home"
|
|
||||||
title={__('Discover Content')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="header__item">
|
|
||||||
<Link
|
|
||||||
onClick={() => navigate('/subscriptions')}
|
|
||||||
button="alt button--flat"
|
|
||||||
icon="icon-at"
|
|
||||||
title={__('My Subscriptions')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="header__item header__item--wunderbar">
|
|
||||||
<WunderBar />
|
<WunderBar />
|
||||||
</div>
|
|
||||||
<div className="header__item">
|
<div className="header__actions-right">
|
||||||
<Link
|
<Button
|
||||||
|
inverse
|
||||||
onClick={() => navigate('/wallet')}
|
onClick={() => navigate('/wallet')}
|
||||||
button="text"
|
icon="user"
|
||||||
className="no-underline"
|
label={isUpgradeAvailable ? `${balance} LBC` : `You have ${balance} LBC`}
|
||||||
icon="icon-bank"
|
description={__('Your wallet')}
|
||||||
label={balance}
|
|
||||||
title={__('Wallet')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="header__item">
|
<Button
|
||||||
<Link
|
|
||||||
onClick={() => navigate('/publish')}
|
onClick={() => navigate('/publish')}
|
||||||
button="primary button--flat"
|
icon="cloud-upload"
|
||||||
icon="icon-upload"
|
label={isUpgradeAvailable ? '' : __('Publish')}
|
||||||
label={__('Publish')}
|
description={__('Publish content')}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="header__item">
|
<Button
|
||||||
<Link
|
alt
|
||||||
onClick={() => navigate('/downloaded')}
|
|
||||||
button="alt button--flat"
|
|
||||||
icon="icon-folder"
|
|
||||||
title={__('Downloads and Publishes')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="header__item">
|
|
||||||
<Link
|
|
||||||
onClick={() => navigate('/settings')}
|
onClick={() => navigate('/settings')}
|
||||||
button="alt button--flat"
|
icon="gear"
|
||||||
icon="icon-gear"
|
description={__('Settings')}
|
||||||
title={__('Settings')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
<Button alt onClick={() => navigate('/help')} icon="question" description={__('Help')} />
|
||||||
{isUpgradeAvailable && (
|
{isUpgradeAvailable && (
|
||||||
<Link
|
<Button onClick={() => downloadUpgrade()} icon="arrow-up" label={__('Upgrade App')} />
|
||||||
onClick={() => downloadUpgrade()}
|
|
||||||
button="primary button--flat"
|
|
||||||
icon="icon-arrow-up"
|
|
||||||
label={__('Upgrade App')}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</header>
|
</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,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'component/icon';
|
import Icon from 'component/common/icon';
|
||||||
import RewardLink from 'component/rewardLink';
|
import RewardLink from 'component/rewardLink';
|
||||||
import rewards from 'rewards.js';
|
import rewards from 'rewards.js';
|
||||||
|
|
||||||
|
|
|
@ -1,60 +1,99 @@
|
||||||
import React from 'react';
|
// @flow
|
||||||
import Icon from 'component/icon';
|
import * as React from 'react';
|
||||||
|
import Icon from 'component/common/icon';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
const Link = props => {
|
type Props = {
|
||||||
|
onClick: ?(any) => any,
|
||||||
|
href: ?string,
|
||||||
|
title: ?string,
|
||||||
|
label: ?string,
|
||||||
|
icon: ?string,
|
||||||
|
iconRight: ?string,
|
||||||
|
disabled: ?boolean,
|
||||||
|
children: ?React.Node,
|
||||||
|
navigate: ?string,
|
||||||
|
// TODO: these (nav) should be a reusable type
|
||||||
|
doNavigate: (string, ?any) => void,
|
||||||
|
navigateParams: any,
|
||||||
|
className: ?string,
|
||||||
|
inverse: ?boolean,
|
||||||
|
circle: ?boolean,
|
||||||
|
alt: ?boolean,
|
||||||
|
flat: ?boolean,
|
||||||
|
fakeLink: ?boolean,
|
||||||
|
description: ?string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Button = (props: Props) => {
|
||||||
const {
|
const {
|
||||||
|
onClick,
|
||||||
href,
|
href,
|
||||||
title,
|
title,
|
||||||
style,
|
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
iconRight,
|
iconRight,
|
||||||
button,
|
|
||||||
disabled,
|
disabled,
|
||||||
children,
|
children,
|
||||||
navigate,
|
navigate,
|
||||||
navigateParams,
|
navigateParams,
|
||||||
doNavigate,
|
doNavigate,
|
||||||
className,
|
className,
|
||||||
span,
|
inverse,
|
||||||
|
alt,
|
||||||
|
circle,
|
||||||
|
flat,
|
||||||
|
fakeLink,
|
||||||
|
description,
|
||||||
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const combinedClassName =
|
const combinedClassName = classnames(
|
||||||
(className || '') +
|
{
|
||||||
(!className && !button ? 'button-text' : '') + // Non-button links get the same look as text buttons
|
btn: !fakeLink,
|
||||||
(button ? ` button-block button-${button} button-set-item` : '') +
|
'btn--link': fakeLink,
|
||||||
(disabled ? ' disabled' : '');
|
'btn--primary': !fakeLink && !alt,
|
||||||
|
'btn--alt': alt,
|
||||||
|
'btn--inverse': inverse,
|
||||||
|
'btn--disabled': disabled,
|
||||||
|
'btn--circle': circle,
|
||||||
|
'btn--flat': flat,
|
||||||
|
},
|
||||||
|
className
|
||||||
|
);
|
||||||
|
|
||||||
const onClick =
|
const extendedOnClick =
|
||||||
!props.onClick && navigate
|
!onClick && navigate
|
||||||
? event => {
|
? event => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
doNavigate(navigate, navigateParams || {});
|
doNavigate(navigate, navigateParams || {});
|
||||||
}
|
}
|
||||||
: props.onClick;
|
: onClick;
|
||||||
|
|
||||||
let content;
|
const content = (
|
||||||
if (children) {
|
<React.Fragment>
|
||||||
content = children;
|
{icon && <Icon icon={icon} fixed />}
|
||||||
} else {
|
{label && <span className="btn__label">{label}</span>}
|
||||||
content = (
|
{children && children}
|
||||||
<span {...('button' in props ? { className: 'button__content' } : {})}>
|
{iconRight && <Icon icon={iconRight} fixed />}
|
||||||
{icon ? <Icon icon={icon} fixed /> : null}
|
</React.Fragment>
|
||||||
{label ? <span className="link-label">{label}</span> : null}
|
|
||||||
{iconRight ? <Icon icon={iconRight} fixed /> : null}
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const linkProps = {
|
return href ? (
|
||||||
className: combinedClassName,
|
<a className={combinedClassName} href={href} title={title}>
|
||||||
href: href || 'javascript:;',
|
{content}
|
||||||
title,
|
</a>
|
||||||
onClick,
|
) : (
|
||||||
style,
|
<button
|
||||||
};
|
aria-label={description || label || title}
|
||||||
|
className={combinedClassName}
|
||||||
return span ? <span {...linkProps}>{content}</span> : <a {...linkProps}>{content}</a>;
|
onClick={extendedOnClick}
|
||||||
|
disabled={disabled}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Link;
|
export default Button;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Icon from 'component/icon';
|
import Icon from 'component/common/icon';
|
||||||
import Link from 'component/link';
|
import Link from 'component/link';
|
||||||
|
|
||||||
export class DropDownMenuItem extends React.PureComponent {
|
export class DropDownMenuItem extends React.PureComponent {
|
||||||
|
|
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 React from 'react';
|
||||||
import Icon from 'component/icon';
|
import { Icon } from 'component/common';
|
||||||
import Link from 'component/link';
|
import Button from 'component/link';
|
||||||
import lbryuri from 'lbryuri';
|
import lbryuri from 'lbryuri';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
class UriIndicator extends React.PureComponent {
|
type Props = {
|
||||||
|
isResolvingUri: boolean,
|
||||||
|
resolveUri: string => void,
|
||||||
|
claim: {
|
||||||
|
channel_name: string,
|
||||||
|
has_signature: boolean,
|
||||||
|
signature_is_valid: boolean,
|
||||||
|
value: {
|
||||||
|
publisherSignature: { certificateId: string },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uri: string,
|
||||||
|
link: ?boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
class UriIndicator extends React.PureComponent<Props> {
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.resolve(this.props);
|
this.resolve(this.props);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps: Props) {
|
||||||
this.resolve(nextProps);
|
this.resolve(nextProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(props) {
|
resolve = (props: Props) => {
|
||||||
const { isResolvingUri, resolveUri, claim, uri } = props;
|
const { isResolvingUri, resolveUri, claim, uri } = props;
|
||||||
|
|
||||||
if (!isResolvingUri && claim === undefined && uri) {
|
if (!isResolvingUri && claim === undefined && uri) {
|
||||||
resolveUri(uri);
|
resolveUri(uri);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { claim, link, uri, isResolvingUri, smallCard, span } = this.props;
|
const { claim, link, isResolvingUri } = this.props;
|
||||||
|
|
||||||
if (isResolvingUri && !claim) {
|
|
||||||
return <span className="empty">Validating...</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!claim) {
|
if (!claim) {
|
||||||
return <span className="empty">Unused</span>;
|
return <span className="empty">{isResolvingUri ? 'Validating...' : 'Unused'}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -38,14 +49,17 @@ class UriIndicator extends React.PureComponent {
|
||||||
signature_is_valid: signatureIsValid,
|
signature_is_valid: signatureIsValid,
|
||||||
value,
|
value,
|
||||||
} = claim;
|
} = claim;
|
||||||
|
|
||||||
const channelClaimId =
|
const channelClaimId =
|
||||||
value && value.publisherSignature && value.publisherSignature.certificateId;
|
value && value.publisherSignature && value.publisherSignature.certificateId;
|
||||||
|
|
||||||
if (!hasSignature || !channelName) {
|
if (!hasSignature || !channelName) {
|
||||||
return <span className="empty">Anonymous</span>;
|
return <span>Anonymous</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let icon, channelLink, modifier;
|
let icon;
|
||||||
|
let channelLink;
|
||||||
|
let modifier;
|
||||||
|
|
||||||
if (signatureIsValid) {
|
if (signatureIsValid) {
|
||||||
modifier = 'valid';
|
modifier = 'valid';
|
||||||
|
@ -59,7 +73,6 @@ class UriIndicator extends React.PureComponent {
|
||||||
<span>
|
<span>
|
||||||
<span
|
<span
|
||||||
className={classnames('channel-name', {
|
className={classnames('channel-name', {
|
||||||
'channel-name--small': smallCard,
|
|
||||||
'button-text no-underline': link,
|
'button-text no-underline': link,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -81,14 +94,9 @@ class UriIndicator extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Button navigate="/show" navigateParams={{ uri: channelLink }} fakeLink>
|
||||||
navigate="/show"
|
|
||||||
navigateParams={{ uri: channelLink }}
|
|
||||||
className="no-underline"
|
|
||||||
span={span}
|
|
||||||
>
|
|
||||||
{inner}
|
{inner}
|
||||||
</Link>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import lbryuri from 'lbryuri.js';
|
import lbryuri from 'lbryuri';
|
||||||
import { selectWunderBarAddress, selectWunderBarIcon } from 'redux/selectors/search';
|
import { selectState as selectSearch, selectWunderBarAddress } from 'redux/selectors/search';
|
||||||
import { doNavigate } from 'redux/actions/navigation';
|
import { doNavigate } from 'redux/actions/navigation';
|
||||||
|
import { updateSearchQuery, getSearchSuggestions } from 'redux/actions/search';
|
||||||
import Wunderbar from './view';
|
import Wunderbar from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
|
...selectSearch(state),
|
||||||
address: selectWunderBarAddress(state),
|
address: selectWunderBarAddress(state),
|
||||||
icon: selectWunderBarIcon(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
onSearch: query => dispatch(doNavigate('/search', { query })),
|
onSearch: query => dispatch(doNavigate('/search', { query })),
|
||||||
onSubmit: (query, extraParams) =>
|
onSubmit: (query, extraParams) =>
|
||||||
dispatch(doNavigate('/show', { uri: lbryuri.normalize(query), ...extraParams })),
|
dispatch(doNavigate('/show', { uri: lbryuri.normalize(query), ...extraParams })),
|
||||||
|
updateSearchQuery: query => dispatch(updateSearchQuery(query)),
|
||||||
|
getSearchSuggestions: query => dispatch(getSearchSuggestions(query)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(Wunderbar);
|
export default connect(select, perform)(Wunderbar);
|
||||||
|
|
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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import lbryuri from 'lbryuri';
|
||||||
import lbryuri from 'lbryuri.js';
|
import classnames from 'classnames';
|
||||||
import Icon from 'component/icon';
|
import Autocomplete from './internal/autocomplete';
|
||||||
import { parseQueryParams } from 'util/query_params';
|
|
||||||
|
|
||||||
class WunderBar extends React.PureComponent {
|
type Props = {
|
||||||
static TYPING_TIMEOUT = 800;
|
updateSearchQuery: string => void,
|
||||||
|
getSearchSuggestions: string => void,
|
||||||
|
onSearch: string => void,
|
||||||
|
onSubmit: string => void,
|
||||||
|
searchQuery: ?string,
|
||||||
|
isActive: boolean,
|
||||||
|
address: ?string,
|
||||||
|
suggestions: Array<string>,
|
||||||
|
};
|
||||||
|
|
||||||
static propTypes = {
|
class WunderBar extends React.PureComponent<Props> {
|
||||||
onSearch: PropTypes.func.isRequired,
|
constructor() {
|
||||||
onSubmit: PropTypes.func.isRequired,
|
super();
|
||||||
};
|
(this: any).handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
(this: any).handleChange = this.handleChange.bind(this);
|
||||||
constructor(props) {
|
(this: any).focus = this.focus.bind(this);
|
||||||
super(props);
|
this.input = undefined;
|
||||||
this._userTypingTimer = null;
|
|
||||||
this._isSearchDispatchPending = false;
|
|
||||||
this._input = null;
|
|
||||||
this._stateBeforeSearch = null;
|
|
||||||
this._resetOnNextBlur = true;
|
|
||||||
this.onChange = this.onChange.bind(this);
|
|
||||||
this.onFocus = this.onFocus.bind(this);
|
|
||||||
this.onBlur = this.onBlur.bind(this);
|
|
||||||
this.onKeyPress = this.onKeyPress.bind(this);
|
|
||||||
this.onReceiveRef = this.onReceiveRef.bind(this);
|
|
||||||
this.state = {
|
|
||||||
address: this.props.address,
|
|
||||||
icon: this.props.icon,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
input: ?HTMLInputElement;
|
||||||
if (this.userTypingTimer) {
|
|
||||||
clearTimeout(this._userTypingTimer);
|
handleChange(e: SyntheticInputEvent<*>) {
|
||||||
|
const { updateSearchQuery, getSearchSuggestions } = this.props;
|
||||||
|
const { value } = e.target;
|
||||||
|
|
||||||
|
updateSearchQuery(value);
|
||||||
|
getSearchSuggestions(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
const { input } = this;
|
||||||
|
if (input) {
|
||||||
|
input.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(event) {
|
handleSubmit(value: string) {
|
||||||
if (this._userTypingTimer) {
|
if (!value) {
|
||||||
clearTimeout(this._userTypingTimer);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ address: event.target.value });
|
const { onSubmit, onSearch } = this.props;
|
||||||
|
|
||||||
this._isSearchDispatchPending = true;
|
// if they choose the "search for {value}" in the suggestions
|
||||||
|
// it will contain the {query}?search
|
||||||
|
const choseDoSuggestedSearch = value.endsWith('?search');
|
||||||
|
|
||||||
const searchQuery = event.target.value;
|
let searchValue = value;
|
||||||
|
if (choseDoSuggestedSearch) {
|
||||||
this._userTypingTimer = setTimeout(() => {
|
searchValue = value.slice(0, -7); // trim off ?search
|
||||||
const hasQuery = searchQuery.length === 0;
|
|
||||||
this._resetOnNextBlur = hasQuery;
|
|
||||||
this._isSearchDispatchPending = false;
|
|
||||||
if (searchQuery) {
|
|
||||||
this.props.onSearch(searchQuery.trim());
|
|
||||||
}
|
|
||||||
}, WunderBar.TYPING_TIMEOUT); // 800ms delay, tweak for faster/slower
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
if (this.input) {
|
||||||
if (
|
this.input.blur();
|
||||||
nextProps.viewingPage !== this.props.viewingPage ||
|
|
||||||
nextProps.address != this.props.address
|
|
||||||
) {
|
|
||||||
this.setState({ address: nextProps.address, icon: nextProps.icon });
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onFocus() {
|
|
||||||
this._stateBeforeSearch = this.state;
|
|
||||||
const newState = {
|
|
||||||
icon: 'icon-search',
|
|
||||||
isActive: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
this._focusPending = true;
|
|
||||||
// below is hacking, improved when we have proper routing
|
|
||||||
if (!this.state.address.startsWith('lbry://') && this.state.icon !== 'icon-search') {
|
|
||||||
// onFocus, if they are not on an exact URL or a search page, clear the bar
|
|
||||||
newState.address = '';
|
|
||||||
}
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
|
||||||
|
|
||||||
onBlur() {
|
|
||||||
if (this._isSearchDispatchPending) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.onBlur();
|
|
||||||
}, WunderBar.TYPING_TIMEOUT + 1);
|
|
||||||
} else {
|
|
||||||
const commonState = { isActive: false };
|
|
||||||
if (this._resetOnNextBlur) {
|
|
||||||
this.setState(Object.assign({}, this._stateBeforeSearch, commonState));
|
|
||||||
this._input.value = this.state.address;
|
|
||||||
} else {
|
|
||||||
this._resetOnNextBlur = true;
|
|
||||||
this._stateBeforeSearch = this.state;
|
|
||||||
this.setState(commonState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
if (this._input) {
|
|
||||||
const start = this._input.selectionStart,
|
|
||||||
end = this._input.selectionEnd;
|
|
||||||
|
|
||||||
this._input.value = this.state.address; // this causes cursor to go to end of input
|
|
||||||
|
|
||||||
this._input.setSelectionRange(start, end);
|
|
||||||
|
|
||||||
if (this._focusPending) {
|
|
||||||
this._input.select();
|
|
||||||
this._focusPending = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyPress(event) {
|
|
||||||
if (event.charCode == 13 && this._input.value) {
|
|
||||||
let uri = null,
|
|
||||||
method = 'onSubmit',
|
|
||||||
extraParams = {};
|
|
||||||
|
|
||||||
this._resetOnNextBlur = false;
|
|
||||||
clearTimeout(this._userTypingTimer);
|
|
||||||
|
|
||||||
const parts = this._input.value.trim().split('?');
|
|
||||||
const value = parts.shift();
|
|
||||||
if (parts.length > 0) extraParams = parseQueryParams(parts.join(''));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
uri = lbryuri.normalize(value);
|
const uri = lbryuri.normalize(value);
|
||||||
this.setState({ value: uri });
|
onSubmit(uri);
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
// then it's not a valid URL, so let's search
|
// search query isn't a valid uri
|
||||||
uri = value;
|
onSearch(searchValue);
|
||||||
method = 'onSearch';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props[method](uri, extraParams);
|
|
||||||
this._input.blur();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onReceiveRef(ref) {
|
|
||||||
this._input = ref;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { searchQuery, isActive, address, suggestions } = this.props;
|
||||||
|
|
||||||
|
// if we are on the file/channel page
|
||||||
|
// use the address in the history stack
|
||||||
|
const wunderbarValue = isActive ? searchQuery : searchQuery || address;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`wunderbar${this.state.isActive ? ' wunderbar--active' : ''}`}>
|
<div
|
||||||
{this.state.icon ? <Icon fixed icon={this.state.icon} /> : ''}
|
className={classnames('header__wunderbar', {
|
||||||
|
'header__wunderbar--active': isActive,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Autocomplete
|
||||||
|
autoHighlight
|
||||||
|
ref={ref => {
|
||||||
|
this.input = ref;
|
||||||
|
}}
|
||||||
|
wrapperStyle={{ flex: 1, minHeight: 0 }}
|
||||||
|
value={wunderbarValue}
|
||||||
|
items={suggestions}
|
||||||
|
getItemValue={item => item.value}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
onSelect={this.handleSubmit}
|
||||||
|
renderInput={props => (
|
||||||
<input
|
<input
|
||||||
|
{...props}
|
||||||
className="wunderbar__input"
|
className="wunderbar__input"
|
||||||
type="search"
|
placeholder="Search for videos, music, games and more"
|
||||||
ref={this.onReceiveRef}
|
/>
|
||||||
onFocus={this.onFocus}
|
)}
|
||||||
onBlur={this.onBlur}
|
renderItem={(item, isHighlighted) => (
|
||||||
onChange={this.onChange}
|
<div
|
||||||
onKeyPress={this.onKeyPress}
|
key={item.value}
|
||||||
value={this.state.address}
|
className={classnames('wunderbar__suggestion', {
|
||||||
placeholder={__('Find videos, music, games, and more')}
|
'wunderbar__active-suggestion': isHighlighted,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -88,9 +88,13 @@ export const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED';
|
||||||
export const FILE_DELETE = 'FILE_DELETE';
|
export const FILE_DELETE = 'FILE_DELETE';
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
export const SEARCH_STARTED = 'SEARCH_STARTED';
|
export const SEARCH_START = 'SEARCH_START';
|
||||||
export const SEARCH_COMPLETED = 'SEARCH_COMPLETED';
|
export const SEARCH_SUCCESS = 'SEARCH_SUCCESS';
|
||||||
export const SEARCH_CANCELLED = 'SEARCH_CANCELLED';
|
export const SEARCH_FAIL = 'SEARCH_FAIL';
|
||||||
|
export const UPDATE_SEARCH_QUERY = 'UPDATE_SEARCH_QUERY';
|
||||||
|
export const SEARCH_SUGGESTIONS_START = 'SEARCH_SUGGESTIONS_START';
|
||||||
|
export const GET_SEARCH_SUGGESTIONS_SUCCESS = 'GET_SEARCH_SUGGESTIONS_SUCCESS';
|
||||||
|
export const GET_SEARCH_SUGGESTIONS_FAIL = 'GET_SEARCH_SUGGESTIONS_FAIL';
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';
|
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';
|
||||||
|
|
|
@ -31,7 +31,7 @@ ipcRenderer.on('open-uri-requested', (event, uri, newSession) => {
|
||||||
try {
|
try {
|
||||||
verification = JSON.parse(atob(uri.substring(15)));
|
verification = JSON.parse(atob(uri.substring(15)));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
if (verification.token && verification.recaptcha) {
|
if (verification.token && verification.recaptcha) {
|
||||||
app.store.dispatch(doConditionalAuthNavigate(newSession));
|
app.store.dispatch(doConditionalAuthNavigate(newSession));
|
||||||
|
@ -112,10 +112,10 @@ const init = () => {
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<div>
|
<React.Fragment>
|
||||||
<App />
|
<App />
|
||||||
<SnackBar />
|
<SnackBar />
|
||||||
</div>
|
</React.Fragment>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
document.getElementById('app')
|
document.getElementById('app')
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import lbryuri from 'lbryuri';
|
import lbryuri from 'lbryuri';
|
||||||
import { BusyMessage } from 'component/common';
|
import { BusyMessage } from 'component/common';
|
||||||
|
@ -5,6 +6,7 @@ import FileTile from 'component/fileTile';
|
||||||
import ReactPaginate from 'react-paginate';
|
import ReactPaginate from 'react-paginate';
|
||||||
import Link from 'component/link';
|
import Link from 'component/link';
|
||||||
import SubscribeButton from 'component/subscribeButton';
|
import SubscribeButton from 'component/subscribeButton';
|
||||||
|
import Page from 'component/page';
|
||||||
|
|
||||||
class ChannelPage extends React.PureComponent {
|
class ChannelPage extends React.PureComponent {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -70,7 +72,7 @@ class ChannelPage extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Page>
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<div className="card__inner">
|
<div className="card__inner">
|
||||||
<div className="card__title-identity">
|
<div className="card__title-identity">
|
||||||
|
@ -107,9 +109,10 @@ class ChannelPage extends React.PureComponent {
|
||||||
containerClassName="pagination"
|
containerClassName="pagination"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChannelPage;
|
export default ChannelPage;
|
||||||
|
/* eslint-enable */
|
||||||
|
|
|
@ -1,259 +1,37 @@
|
||||||
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import Page from 'component/page';
|
||||||
import lbryuri from 'lbryuri';
|
import CategoryList from 'component/common/category-list';
|
||||||
import FileCard from 'component/fileCard';
|
|
||||||
import { BusyMessage } from 'component/common.js';
|
|
||||||
import Icon from 'component/icon';
|
|
||||||
import ToolTip from 'component/tooltip.js';
|
|
||||||
import SubHeader from 'component/subHeader';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import Link from 'component/link';
|
|
||||||
|
|
||||||
// This should be in a separate file
|
type Props = {
|
||||||
export class FeaturedCategory extends React.PureComponent {
|
fetchFeaturedUris: () => void,
|
||||||
constructor() {
|
fetchingFeaturedUris: boolean,
|
||||||
super();
|
featuredUris: {},
|
||||||
|
};
|
||||||
|
|
||||||
this.state = {
|
class DiscoverPage extends React.PureComponent<Props> {
|
||||||
numItems: undefined,
|
|
||||||
canScrollPrevious: false,
|
|
||||||
canScrollNext: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
this.setState({
|
|
||||||
numItems: this.props.names.length,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const cardRow = ReactDOM.findDOMNode(this.refs.rowitems);
|
|
||||||
const cards = cardRow.getElementsByTagName('section');
|
|
||||||
|
|
||||||
// check if the last card is visible
|
|
||||||
const lastCard = cards[cards.length - 1];
|
|
||||||
const isCompletelyVisible = this.isCardVisible(lastCard, cardRow, false);
|
|
||||||
|
|
||||||
if (!isCompletelyVisible) {
|
|
||||||
this.setState({
|
|
||||||
canScrollNext: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleScrollPrevious() {
|
|
||||||
const cardRow = ReactDOM.findDOMNode(this.refs.rowitems);
|
|
||||||
if (cardRow.scrollLeft > 0) {
|
|
||||||
// check the visible cards
|
|
||||||
const cards = cardRow.getElementsByTagName('section');
|
|
||||||
let firstVisibleCard = null;
|
|
||||||
let firstVisibleIdx = -1;
|
|
||||||
for (let i = 0; i < cards.length; i++) {
|
|
||||||
if (this.isCardVisible(cards[i], cardRow, false)) {
|
|
||||||
firstVisibleCard = cards[i];
|
|
||||||
firstVisibleIdx = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const numDisplayed = this.numDisplayedCards(cardRow);
|
|
||||||
const scrollToIdx = firstVisibleIdx - numDisplayed;
|
|
||||||
const animationCallback = () => {
|
|
||||||
this.setState({
|
|
||||||
canScrollPrevious: cardRow.scrollLeft !== 0,
|
|
||||||
canScrollNext: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
this.scrollCardItemsLeftAnimated(
|
|
||||||
cardRow,
|
|
||||||
scrollToIdx < 0 ? 0 : cards[scrollToIdx].offsetLeft,
|
|
||||||
100,
|
|
||||||
animationCallback
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleScrollNext() {
|
|
||||||
const cardRow = ReactDOM.findDOMNode(this.refs.rowitems);
|
|
||||||
|
|
||||||
// check the visible cards
|
|
||||||
const cards = cardRow.getElementsByTagName('section');
|
|
||||||
let lastVisibleCard = null;
|
|
||||||
let lastVisibleIdx = -1;
|
|
||||||
for (let i = 0; i < cards.length; i++) {
|
|
||||||
if (this.isCardVisible(cards[i], cardRow, true)) {
|
|
||||||
lastVisibleCard = cards[i];
|
|
||||||
lastVisibleIdx = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastVisibleCard) {
|
|
||||||
const numDisplayed = this.numDisplayedCards(cardRow);
|
|
||||||
const animationCallback = () => {
|
|
||||||
// update last visible index after scroll
|
|
||||||
for (let i = 0; i < cards.length; i++) {
|
|
||||||
if (this.isCardVisible(cards[i], cardRow, true)) {
|
|
||||||
lastVisibleIdx = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ canScrollPrevious: true });
|
|
||||||
if (lastVisibleIdx === cards.length - 1) {
|
|
||||||
this.setState({ canScrollNext: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.scrollCardItemsLeftAnimated(
|
|
||||||
cardRow,
|
|
||||||
Math.min(lastVisibleCard.offsetLeft, cardRow.scrollWidth - cardRow.clientWidth),
|
|
||||||
100,
|
|
||||||
animationCallback
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollCardItemsLeftAnimated(cardRow, target, duration, callback) {
|
|
||||||
if (!duration || duration <= diff) {
|
|
||||||
cardRow.scrollLeft = target;
|
|
||||||
if (callback) {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const component = this;
|
|
||||||
const diff = target - cardRow.scrollLeft;
|
|
||||||
const tick = diff / duration * 10;
|
|
||||||
setTimeout(() => {
|
|
||||||
cardRow.scrollLeft += tick;
|
|
||||||
if (cardRow.scrollLeft === target) {
|
|
||||||
if (callback) {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
component.scrollCardItemsLeftAnimated(cardRow, target, duration - 10, callback);
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
isCardVisible(section, cardRow, partialVisibility) {
|
|
||||||
// check if a card is fully or partialy visible in its parent
|
|
||||||
const cardRowWidth = cardRow.offsetWidth;
|
|
||||||
const cardRowLeft = cardRow.scrollLeft;
|
|
||||||
const cardRowEnd = cardRowLeft + cardRow.offsetWidth;
|
|
||||||
const sectionLeft = section.offsetLeft - cardRowLeft;
|
|
||||||
const sectionEnd = sectionLeft + section.offsetWidth;
|
|
||||||
|
|
||||||
return (
|
|
||||||
(sectionLeft >= 0 && sectionEnd <= cardRowWidth) ||
|
|
||||||
(((sectionLeft < 0 && sectionEnd > 0) || (sectionLeft > 0 && sectionLeft <= cardRowWidth)) &&
|
|
||||||
partialVisibility)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
numDisplayedCards(cardRow) {
|
|
||||||
const cards = cardRow.getElementsByTagName('section');
|
|
||||||
const cardRowWidth = cardRow.offsetWidth;
|
|
||||||
// get the width of the first card and then calculate
|
|
||||||
const cardWidth = cards.length > 0 ? cards[0].offsetWidth : 0;
|
|
||||||
|
|
||||||
if (cardWidth > 0) {
|
|
||||||
return Math.ceil(cardRowWidth / cardWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
// return a default value of 1 card displayed if the card width couldn't be determined
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { category, names, categoryLink } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card-row card-row--small">
|
|
||||||
<h3 className="card-row__header">
|
|
||||||
{categoryLink ? (
|
|
||||||
<Link
|
|
||||||
className="button-text no-underline"
|
|
||||||
label={category}
|
|
||||||
navigate="/show"
|
|
||||||
navigateParams={{ uri: categoryLink }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
category
|
|
||||||
)}
|
|
||||||
|
|
||||||
{category &&
|
|
||||||
category.match(/^community/i) && (
|
|
||||||
<ToolTip
|
|
||||||
label={__("What's this?")}
|
|
||||||
body={__(
|
|
||||||
'Community Content is a public space where anyone can share content with the rest of the LBRY community. Bid on the names "one," "two," "three," "four" and "five" to put your content here!'
|
|
||||||
)}
|
|
||||||
className="tooltip--header"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
<div className="card-row__scrollhouse">
|
|
||||||
{this.state.canScrollPrevious && (
|
|
||||||
<div className="card-row__nav card-row__nav--left">
|
|
||||||
<a className="card-row__scroll-button" onClick={this.handleScrollPrevious.bind(this)}>
|
|
||||||
<Icon icon="icon-chevron-left" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{this.state.canScrollNext && (
|
|
||||||
<div className="card-row__nav card-row__nav--right">
|
|
||||||
<a className="card-row__scroll-button" onClick={this.handleScrollNext.bind(this)}>
|
|
||||||
<Icon icon="icon-chevron-right" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div ref="rowitems" className="card-row__items">
|
|
||||||
{names &&
|
|
||||||
names.map(name => (
|
|
||||||
<FileCard key={name} displayStyle="card" uri={lbryuri.normalize(name)} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DiscoverPage extends React.PureComponent {
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.props.fetchFeaturedUris();
|
this.props.fetchFeaturedUris();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { featuredUris, fetchingFeaturedUris } = this.props;
|
const { featuredUris, fetchingFeaturedUris } = this.props;
|
||||||
const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length,
|
const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length;
|
||||||
failedToLoad = !fetchingFeaturedUris && !hasContent;
|
const failedToLoad = !fetchingFeaturedUris && !hasContent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<Page noPadding isLoading={!hasContent && fetchingFeaturedUris}>
|
||||||
className={classnames('main main--no-margin', {
|
|
||||||
reloading: hasContent && fetchingFeaturedUris,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<SubHeader fullWidth smallMargin />
|
|
||||||
{!hasContent && fetchingFeaturedUris && <BusyMessage message={__('Fetching content')} />}
|
|
||||||
{hasContent &&
|
{hasContent &&
|
||||||
Object.keys(featuredUris).map(
|
Object.keys(featuredUris).map(
|
||||||
category =>
|
category =>
|
||||||
featuredUris[category].length ? (
|
featuredUris[category].length ? (
|
||||||
<FeaturedCategory
|
<CategoryList key={category} category={category} names={featuredUris[category]} />
|
||||||
key={category}
|
|
||||||
category={category}
|
|
||||||
names={featuredUris[category]}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{failedToLoad && <div className="empty">{__('Failed to load landing content.')}</div>}
|
{failedToLoad && <div className="empty">{__('Failed to load landing content.')}</div>}
|
||||||
</main>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import lbry from 'lbry';
|
import lbry from 'lbry';
|
||||||
import lbryuri from 'lbryuri';
|
import lbryuri from 'lbryuri';
|
||||||
|
@ -6,12 +7,13 @@ import { Thumbnail } from 'component/common';
|
||||||
import FilePrice from 'component/filePrice';
|
import FilePrice from 'component/filePrice';
|
||||||
import FileDetails from 'component/fileDetails';
|
import FileDetails from 'component/fileDetails';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import Icon from 'component/icon';
|
import Icon from 'component/common/icon';
|
||||||
import WalletSendTip from 'component/walletSendTip';
|
import WalletSendTip from 'component/walletSendTip';
|
||||||
import DateTime from 'component/dateTime';
|
import DateTime from 'component/dateTime';
|
||||||
import * as icons from 'constants/icons';
|
import * as icons from 'constants/icons';
|
||||||
import Link from 'component/link';
|
import Link from 'component/link';
|
||||||
import SubscribeButton from 'component/subscribeButton';
|
import SubscribeButton from 'component/subscribeButton';
|
||||||
|
import Page from 'component/page';
|
||||||
|
|
||||||
class FilePage extends React.PureComponent {
|
class FilePage extends React.PureComponent {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -69,6 +71,7 @@ class FilePage extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Page>
|
||||||
<section className={`card ${obscureNsfw ? 'card--obscured ' : ''}`}>
|
<section className={`card ${obscureNsfw ? 'card--obscured ' : ''}`}>
|
||||||
<div className="show-page-media">
|
<div className="show-page-media">
|
||||||
{isPlayable ? (
|
{isPlayable ? (
|
||||||
|
@ -110,8 +113,10 @@ class FilePage extends React.PureComponent {
|
||||||
{tab === 'tip' && <WalletSendTip claim_id={claim.claim_id} uri={uri} />}
|
{tab === 'tip' && <WalletSendTip claim_id={claim.claim_id} uri={uri} />}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FilePage;
|
export default FilePage;
|
||||||
|
/* eslint-enable */
|
||||||
|
|
|
@ -4,7 +4,7 @@ import lbry from 'lbry.js';
|
||||||
import Link from 'component/link';
|
import Link from 'component/link';
|
||||||
import SubHeader from 'component/subHeader';
|
import SubHeader from 'component/subHeader';
|
||||||
import { BusyMessage } from 'component/common';
|
import { BusyMessage } from 'component/common';
|
||||||
import Icon from 'component/icon';
|
import Icon from 'component/common/icon';
|
||||||
|
|
||||||
class HelpPage extends React.PureComponent {
|
class HelpPage extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import lbryuri from 'lbryuri';
|
import lbryuri from 'lbryuri';
|
||||||
import FileTile from 'component/fileTile';
|
import FileTile from 'component/fileTile';
|
||||||
import FileListSearch from 'component/fileListSearch';
|
import FileListSearch from 'component/fileListSearch';
|
||||||
import { ToolTip } from 'component/tooltip.js';
|
import ToolTip from 'component/common/tooltip';
|
||||||
|
import Page from 'component/page';
|
||||||
|
|
||||||
class SearchPage extends React.PureComponent {
|
type Props = {
|
||||||
|
query: ?string,
|
||||||
|
};
|
||||||
|
|
||||||
|
class SearchPage extends React.PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { query } = this.props;
|
const { query } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="main--single-column">
|
<Page>
|
||||||
{lbryuri.isValid(query) ? (
|
{lbryuri.isValid(query) ? (
|
||||||
<section className="section-spaced">
|
<section className="section-spaced">
|
||||||
<h3 className="card-row__header">
|
<h3 className="card-row__header">
|
||||||
|
@ -36,7 +42,7 @@ class SearchPage extends React.PureComponent {
|
||||||
</h3>
|
</h3>
|
||||||
<FileListSearch query={query} />
|
<FileListSearch query={query} />
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
|
/* eslint-disable */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import lbryuri from 'lbryuri';
|
|
||||||
import { BusyMessage } from 'component/common';
|
import { BusyMessage } from 'component/common';
|
||||||
import ChannelPage from 'page/channel';
|
import ChannelPage from 'page/channel';
|
||||||
import FilePage from 'page/file';
|
import FilePage from 'page/file';
|
||||||
|
|
||||||
class ShowPage extends React.PureComponent {
|
type Props = {
|
||||||
|
isResolvingUri: boolean,
|
||||||
|
resolveUri: string => void,
|
||||||
|
uri: string,
|
||||||
|
claim: { name: string },
|
||||||
|
};
|
||||||
|
|
||||||
|
class ShowPage extends React.PureComponent<Props> {
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
const { isResolvingUri, resolveUri, uri } = this.props;
|
const { isResolvingUri, resolveUri, uri } = this.props;
|
||||||
|
|
||||||
if (!isResolvingUri) resolveUri(uri);
|
if (!isResolvingUri) resolveUri(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps: Props) {
|
||||||
const { isResolvingUri, resolveUri, claim, uri } = nextProps;
|
const { isResolvingUri, resolveUri, claim, uri } = nextProps;
|
||||||
|
|
||||||
if (!isResolvingUri && claim === undefined && uri) {
|
if (!isResolvingUri && claim === undefined && uri) {
|
||||||
|
@ -47,8 +54,9 @@ class ShowPage extends React.PureComponent {
|
||||||
innerContent = <FilePage uri={uri} />;
|
innerContent = <FilePage uri={uri} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <main className="main--single-column">{innerContent}</main>;
|
return innerContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ShowPage;
|
export default ShowPage;
|
||||||
|
/* eslint-enable */
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SubHeader from 'component/subHeader';
|
import SubHeader from 'component/subHeader';
|
||||||
import { BusyMessage } from 'component/common.js';
|
import { BusyMessage } from 'component/common';
|
||||||
import { FeaturedCategory } from 'page/discover/view';
|
import CategoryList from 'component/common/category-list';
|
||||||
import type { Subscription } from 'redux/reducers/subscriptions';
|
import type { Subscription } from 'redux/reducers/subscriptions';
|
||||||
|
|
||||||
type SavedSubscriptions = Array<Subscription>;
|
type SavedSubscriptions = Array<Subscription>;
|
||||||
|
@ -83,7 +83,7 @@ export default class extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FeaturedCategory
|
<CategoryList
|
||||||
key={subscription.channelName}
|
key={subscription.channelName}
|
||||||
categoryLink={subscription.uri}
|
categoryLink={subscription.uri}
|
||||||
category={subscription.channelName}
|
category={subscription.channelName}
|
||||||
|
|
|
@ -5,9 +5,13 @@ import { doNavigate } from 'redux/actions/navigation';
|
||||||
import { selectCurrentPage } from 'redux/selectors/navigation';
|
import { selectCurrentPage } from 'redux/selectors/navigation';
|
||||||
import batchActions from 'util/batchActions';
|
import batchActions from 'util/batchActions';
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// TODO: this should be in a util
|
||||||
export function doSearch(rawQuery) {
|
const handleSearchApiResponse = searchResponse =>
|
||||||
return (dispatch, getState) => {
|
searchResponse.status === 200
|
||||||
|
? Promise.resolve(searchResponse.json())
|
||||||
|
: Promise.reject(new Error(searchResponse.statusText));
|
||||||
|
|
||||||
|
export const doSearch = rawQuery => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const page = selectCurrentPage(state);
|
const page = selectCurrentPage(state);
|
||||||
|
|
||||||
|
@ -15,13 +19,13 @@ export function doSearch(rawQuery) {
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.SEARCH_CANCELLED,
|
type: ACTIONS.SEARCH_FAIL,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.SEARCH_STARTED,
|
type: ACTIONS.SEARCH_START,
|
||||||
data: { query },
|
data: { query },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -29,12 +33,7 @@ export function doSearch(rawQuery) {
|
||||||
dispatch(doNavigate('search', { query }));
|
dispatch(doNavigate('search', { query }));
|
||||||
} else {
|
} else {
|
||||||
fetch(`https://lighthouse.lbry.io/search?s=${query}`)
|
fetch(`https://lighthouse.lbry.io/search?s=${query}`)
|
||||||
.then(
|
.then(handleSearchApiResponse)
|
||||||
response =>
|
|
||||||
response.status === 200
|
|
||||||
? Promise.resolve(response.json())
|
|
||||||
: Promise.reject(new Error(response.statusText))
|
|
||||||
)
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const uris = [];
|
const uris = [];
|
||||||
const actions = [];
|
const actions = [];
|
||||||
|
@ -49,7 +48,7 @@ export function doSearch(rawQuery) {
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.push({
|
actions.push({
|
||||||
type: ACTIONS.SEARCH_COMPLETED,
|
type: ACTIONS.SEARCH_SUCCESS,
|
||||||
data: {
|
data: {
|
||||||
query,
|
query,
|
||||||
uris,
|
uris,
|
||||||
|
@ -59,9 +58,66 @@ export function doSearch(rawQuery) {
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.SEARCH_CANCELLED,
|
type: ACTIONS.SEARCH_FAIL,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
export const updateSearchQuery = searchQuery => ({
|
||||||
|
type: ACTIONS.UPDATE_SEARCH_QUERY,
|
||||||
|
data: { searchQuery },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getSearchSuggestions = value => dispatch => {
|
||||||
|
dispatch({ type: ACTIONS.SEARCH_SUGGESTIONS_START });
|
||||||
|
if (!value) {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.GET_SEARCH_SUGGESTIONS_SUCCESS,
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should probably be more robust
|
||||||
|
let searchValue = value;
|
||||||
|
if (searchValue.startsWith('lbry://')) {
|
||||||
|
searchValue = searchValue.slice(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
// need to handle spaces in the query?
|
||||||
|
fetch(`https://lighthouse.lbry.io/autocomplete?s=${searchValue}`)
|
||||||
|
.then(handleSearchApiResponse)
|
||||||
|
.then(suggestions => {
|
||||||
|
const formattedSuggestions = suggestions.slice(0, 5).map(suggestion => ({
|
||||||
|
label: suggestion,
|
||||||
|
value: suggestion,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Should we add lbry://{query} as the first result?
|
||||||
|
// If it's not a valid uri, then add a "search for {query}" result
|
||||||
|
const searchLabel = `Search for "${value}"`;
|
||||||
|
try {
|
||||||
|
const uri = Lbryuri.normalize(value);
|
||||||
|
formattedSuggestions.unshift(
|
||||||
|
{ label: uri, value: uri },
|
||||||
|
{ label: searchLabel, value: `${value}?search` }
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (value) {
|
||||||
|
formattedSuggestions.unshift({ label: searchLabel, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dispatch({
|
||||||
|
type: ACTIONS.GET_SEARCH_SUGGESTIONS_SUCCESS,
|
||||||
|
data: formattedSuggestions,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err =>
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.GET_SEARCH_SUGGESTIONS_FAIL,
|
||||||
|
data: err,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -1,32 +1,75 @@
|
||||||
|
// @flow
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import { handleActions } from 'util/redux-utils';
|
||||||
|
|
||||||
const reducers = {};
|
type SearchState = {
|
||||||
const defaultState = {
|
isActive: boolean,
|
||||||
|
searchQuery: string,
|
||||||
|
searchingForSuggestions: boolean,
|
||||||
|
suggestions: Array<string>,
|
||||||
urisByQuery: {},
|
urisByQuery: {},
|
||||||
searching: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
reducers[ACTIONS.SEARCH_STARTED] = state =>
|
const defaultState = {
|
||||||
Object.assign({}, state, {
|
isActive: false,
|
||||||
searching: true,
|
searchQuery: '', // needs to be an empty string for input focusing
|
||||||
});
|
searchingForSuggestions: false,
|
||||||
|
suggestions: [],
|
||||||
|
urisByQuery: {},
|
||||||
|
};
|
||||||
|
|
||||||
reducers[ACTIONS.SEARCH_COMPLETED] = (state, action) => {
|
export default handleActions(
|
||||||
|
{
|
||||||
|
[ACTIONS.SEARCH_START]: (state: SearchState): SearchState => ({
|
||||||
|
...state,
|
||||||
|
searching: true,
|
||||||
|
}),
|
||||||
|
[ACTIONS.SEARCH_SUCCESS]: (state: SearchState, action): SearchState => {
|
||||||
const { query, uris } = action.data;
|
const { query, uris } = action.data;
|
||||||
|
|
||||||
return Object.assign({}, state, {
|
return {
|
||||||
|
...state,
|
||||||
searching: false,
|
searching: false,
|
||||||
urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }),
|
urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }),
|
||||||
});
|
};
|
||||||
};
|
},
|
||||||
|
|
||||||
reducers[ACTIONS.SEARCH_CANCELLED] = state =>
|
[ACTIONS.SEARCH_FAIL]: (state: SearchState): SearchState => ({
|
||||||
Object.assign({}, state, {
|
...state,
|
||||||
searching: false,
|
searching: false,
|
||||||
});
|
}),
|
||||||
|
|
||||||
export default function reducer(state = defaultState, action) {
|
[ACTIONS.UPDATE_SEARCH_QUERY]: (state: SearchState, action): SearchState => ({
|
||||||
const handler = reducers[action.type];
|
...state,
|
||||||
if (handler) return handler(state, action);
|
searchQuery: action.data.searchQuery,
|
||||||
return state;
|
suggestions: [],
|
||||||
}
|
isActive: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
[ACTIONS.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,
|
selectCurrentParams,
|
||||||
(page, params) => {
|
(page, params) => {
|
||||||
switch (page) {
|
switch (page) {
|
||||||
case 'settings':
|
|
||||||
return __('Settings');
|
|
||||||
case 'report':
|
|
||||||
return __('Report');
|
|
||||||
case 'wallet':
|
|
||||||
return __('Wallet');
|
|
||||||
case 'send':
|
|
||||||
return __('Send or Receive LBRY Credits');
|
|
||||||
case 'getcredits':
|
|
||||||
return __('Get LBRY Credits');
|
|
||||||
case 'backup':
|
|
||||||
return __('Backup Your Wallet');
|
|
||||||
case 'rewards':
|
|
||||||
return __('Rewards');
|
|
||||||
case 'invite':
|
|
||||||
return __('Invites');
|
|
||||||
case 'start':
|
|
||||||
return __('Start');
|
|
||||||
case 'publish':
|
|
||||||
return params.id ? __('Edit') : __('Publish');
|
|
||||||
case 'help':
|
|
||||||
return __('Help');
|
|
||||||
case 'developer':
|
|
||||||
return __('Developer');
|
|
||||||
case 'show': {
|
case 'show': {
|
||||||
const parts = [Lbryuri.normalize(params.uri)];
|
const parts = [Lbryuri.normalize(params.uri)];
|
||||||
// If the params has any keys other than "uri"
|
// If the params has any keys other than "uri"
|
||||||
|
@ -100,21 +76,14 @@ export const selectPageTitle = createSelector(
|
||||||
}
|
}
|
||||||
return parts.join('?');
|
return parts.join('?');
|
||||||
}
|
}
|
||||||
case 'downloaded':
|
|
||||||
return __('Downloads & Purchases');
|
|
||||||
case 'published':
|
|
||||||
return __('Publications');
|
|
||||||
case 'search':
|
|
||||||
return params.query ? __('Search results for %s', params.query) : __('Search');
|
|
||||||
case 'subscriptions':
|
|
||||||
return __('Your Subscriptions');
|
|
||||||
case 'discover':
|
case 'discover':
|
||||||
|
return __('Discover');
|
||||||
case false:
|
case false:
|
||||||
case null:
|
case null:
|
||||||
case '':
|
case '':
|
||||||
return '';
|
return '';
|
||||||
default:
|
default:
|
||||||
return page[0].toUpperCase() + (page.length > 0 ? page.substr(1) : '');
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -28,52 +28,15 @@ export const selectWunderBarAddress = createSelector(
|
||||||
selectCurrentPage,
|
selectCurrentPage,
|
||||||
selectPageTitle,
|
selectPageTitle,
|
||||||
selectSearchQuery,
|
selectSearchQuery,
|
||||||
(page, title, query) => (page !== 'search' ? title : query || title)
|
(page, title, query) => {
|
||||||
);
|
// only populate the wunderbar address if we are on the file/channel pages
|
||||||
|
// or show the search query
|
||||||
|
if (page === 'show') {
|
||||||
|
return title;
|
||||||
|
} else if (page === 'search') {
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
export const selectWunderBarIcon = createSelector(
|
return '';
|
||||||
selectCurrentPage,
|
|
||||||
selectCurrentParams,
|
|
||||||
(page, params) => {
|
|
||||||
switch (page) {
|
|
||||||
case 'auth':
|
|
||||||
return 'icon-user';
|
|
||||||
case 'settings':
|
|
||||||
return 'icon-gear';
|
|
||||||
case 'help':
|
|
||||||
return 'icon-question';
|
|
||||||
case 'report':
|
|
||||||
return 'icon-file';
|
|
||||||
case 'downloaded':
|
|
||||||
return 'icon-folder';
|
|
||||||
case 'published':
|
|
||||||
return 'icon-folder';
|
|
||||||
case 'history':
|
|
||||||
return 'icon-history';
|
|
||||||
case 'send':
|
|
||||||
return 'icon-send';
|
|
||||||
case 'rewards':
|
|
||||||
return 'icon-rocket';
|
|
||||||
case 'invite':
|
|
||||||
return 'icon-envelope-open';
|
|
||||||
case 'getcredits':
|
|
||||||
return 'icon-shopping-cart';
|
|
||||||
case 'wallet':
|
|
||||||
case 'backup':
|
|
||||||
return 'icon-bank';
|
|
||||||
case 'show':
|
|
||||||
return 'icon-file';
|
|
||||||
case 'publish':
|
|
||||||
return params.id ? __('icon-pencil') : __('icon-upload');
|
|
||||||
case 'developer':
|
|
||||||
return 'icon-code';
|
|
||||||
case 'discover':
|
|
||||||
case 'search':
|
|
||||||
return 'icon-search';
|
|
||||||
case 'subscriptions':
|
|
||||||
return 'icon-th-list';
|
|
||||||
default:
|
|
||||||
return 'icon-file';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,30 @@
|
||||||
@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
|
||||||
|
|
||||||
|
// The actual fonts used will change ex: medium vs regular
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Metropolis';
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
src: url('../../../static/font/metropolis/Metropolis-Medium.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Metropolis';
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
src: url('../../../static/font/metropolis/Metropolis-SemiBold.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Metropolis';
|
||||||
|
font-weight: 800;
|
||||||
|
font-style: normal;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
src: url('../../../static/font/metropolis/Metropolis-ExtraBold.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -7,84 +33,20 @@ html {
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-family: 'Roboto', sans-serif;
|
font-family: 'Metropolis', sans-serif;
|
||||||
line-height: var(--font-line-height);
|
line-height: var(--font-line-height);
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom text selection */
|
h1,
|
||||||
*::selection {
|
h2,
|
||||||
background: var(--text-selection-bg);
|
h3,
|
||||||
color: var(--text-selection-color);
|
h4,
|
||||||
}
|
h5 {
|
||||||
|
|
||||||
#window {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: var(--window-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.credit-amount--indicator {
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-money);
|
|
||||||
}
|
|
||||||
.credit-amount--fee {
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: var(--color-meta-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.credit-amount--bold {
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
#main-content {
|
|
||||||
margin: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: overlay;
|
|
||||||
padding: $spacing-vertical;
|
|
||||||
position: absolute;
|
|
||||||
top: var(--header-height);
|
|
||||||
bottom: 0;
|
|
||||||
left: 4px;
|
|
||||||
right: 4px;
|
|
||||||
main {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
main.main--single-column {
|
|
||||||
width: $width-page-constrained;
|
|
||||||
}
|
|
||||||
|
|
||||||
main.main--no-margin {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reloading {
|
|
||||||
&:before {
|
|
||||||
$width: 30px;
|
|
||||||
position: absolute;
|
|
||||||
background: url('../../../static/img/busy.gif') no-repeat center center;
|
|
||||||
width: $width;
|
|
||||||
height: $spacing-vertical;
|
|
||||||
content: '';
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -1 / 2 * $width;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-fixed-width {
|
|
||||||
/* This borrowed is from a component of Font Awesome we're not using, maybe add it? */
|
|
||||||
width: (18em / 14);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon--left-pad {
|
|
||||||
padding-left: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.75em;
|
font-size: 1.75em;
|
||||||
}
|
}
|
||||||
|
@ -100,11 +62,13 @@ h4 {
|
||||||
h5 {
|
h5 {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
sup,
|
sup,
|
||||||
sub {
|
sub {
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
sup {
|
sup {
|
||||||
top: -0.4em;
|
top: -0.4em;
|
||||||
}
|
}
|
||||||
|
@ -117,11 +81,67 @@ code {
|
||||||
background-color: var(--color-bg-alt);
|
background-color: var(--color-bg-alt);
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
// Without this buttons don't have the Metropolis font
|
||||||
margin-bottom: 0.8em;
|
button {
|
||||||
&:last-child {
|
font-family: inherit;
|
||||||
margin-bottom: 0;
|
}
|
||||||
}
|
|
||||||
|
#window {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
// don't use {bottom/top} here
|
||||||
|
// they cause flashes of un-rendered content when scrolling
|
||||||
|
margin-top: var(--header-height);
|
||||||
|
// TODO: fix this scrollbar extends beyond screen at the bottom
|
||||||
|
padding-bottom: var(--header-height);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 0 $spacing-vertical * 2/3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main--no-padding {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page__header {
|
||||||
|
padding: $spacing-vertical * 2/3;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page__title {
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom text selection */
|
||||||
|
*::selection {
|
||||||
|
background: var(--text-selection-bg);
|
||||||
|
color: var(--text-selection-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.credit-amount--indicator {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-money);
|
||||||
|
}
|
||||||
|
|
||||||
|
.credit-amount--fee {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--color-meta-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.credit-amount--bold {
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
|
@ -192,7 +212,3 @@ p {
|
||||||
section.section-spaced {
|
section.section-spaced {
|
||||||
margin-bottom: $spacing-vertical;
|
margin-bottom: $spacing-vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
|
@ -27,6 +27,16 @@
|
||||||
transform: translate(0, 0);
|
transform: translate(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon--fixed-width {
|
||||||
|
/* This borrowed is from a component of Font Awesome we're not using, maybe add it? */
|
||||||
|
width: (18em / 14);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon--padded {
|
||||||
|
padding: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Adjustments for icon size and alignment */
|
/* Adjustments for icon size and alignment */
|
||||||
.icon-rocket {
|
.icon-rocket {
|
||||||
color: orangered;
|
color: orangered;
|
||||||
|
|
|
@ -6,6 +6,8 @@ $width-page-constrained: 800px;
|
||||||
$text-color: #000;
|
$text-color: #000;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--spacing-vertical: 24px;
|
||||||
|
|
||||||
/* Colors */
|
/* Colors */
|
||||||
--color-brand: #155b4a;
|
--color-brand: #155b4a;
|
||||||
--color-primary: #155b4a;
|
--color-primary: #155b4a;
|
||||||
|
@ -21,7 +23,8 @@ $text-color: #000;
|
||||||
--color-download: rgba(0, 0, 0, 0.75);
|
--color-download: rgba(0, 0, 0, 0.75);
|
||||||
--color-canvas: #f5f5f5;
|
--color-canvas: #f5f5f5;
|
||||||
--color-bg: #ffffff;
|
--color-bg: #ffffff;
|
||||||
--color-bg-alt: #d9d9d9;
|
--color-bg-alt: #f6f6f6;
|
||||||
|
--color-placeholder: #ececec;
|
||||||
|
|
||||||
/* Misc */
|
/* Misc */
|
||||||
--content-max-width: 1000px;
|
--content-max-width: 1000px;
|
||||||
|
@ -34,7 +37,7 @@ $text-color: #000;
|
||||||
--font-size-subtext-multiple: 0.82;
|
--font-size-subtext-multiple: 0.82;
|
||||||
|
|
||||||
/* Shadows */
|
/* Shadows */
|
||||||
--box-shadow-layer: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
--box-shadow-layer: 0px 1px 3px 0px rgba(0, 0, 0, 0.2);
|
||||||
--box-shadow-focus: 2px 4px 4px 0 rgba(0, 0, 0, 0.14), 2px 5px 3px -2px rgba(0, 0, 0, 0.2),
|
--box-shadow-focus: 2px 4px 4px 0 rgba(0, 0, 0, 0.14), 2px 5px 3px -2px rgba(0, 0, 0, 0.2),
|
||||||
2px 3px 7px 0 rgba(0, 0, 0, 0.12);
|
2px 3px 7px 0 rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
|
@ -50,9 +53,6 @@ $text-color: #000;
|
||||||
--text-selection-bg: rgba(saturate(lighten(#155b4a, 20%), 20%), 1); // temp color
|
--text-selection-bg: rgba(saturate(lighten(#155b4a, 20%), 20%), 1); // temp color
|
||||||
--text-selection-color: #fff;
|
--text-selection-color: #fff;
|
||||||
|
|
||||||
/* Window */
|
|
||||||
--window-bg: var(--color-canvas);
|
|
||||||
|
|
||||||
/* Form */
|
/* Form */
|
||||||
--form-label-color: rgba(0, 0, 0, 0.54);
|
--form-label-color: rgba(0, 0, 0, 0.54);
|
||||||
|
|
||||||
|
@ -80,21 +80,23 @@ $text-color: #000;
|
||||||
--select-bg: var(--color-bg-alt);
|
--select-bg: var(--color-bg-alt);
|
||||||
--select-color: var(--text-color);
|
--select-color: var(--text-color);
|
||||||
|
|
||||||
|
//TODO: determine proper button variables;
|
||||||
/* Button */
|
/* Button */
|
||||||
--button-bg: var(--color-bg-alt);
|
--btn-primary-color: #fff;
|
||||||
--button-color: #fff;
|
--button-alt-color: var(--text-color);
|
||||||
--button-primary-bg: var(--color-primary);
|
--btn-primary-bg: var(--color-primary);
|
||||||
--button-primary-color: #fff;
|
--btn-alt-bg: red;
|
||||||
--button-padding: $spacing-vertical * 2/3;
|
--btn-radius: 10px;
|
||||||
--button-height: $spacing-vertical * 1.5;
|
// below needed?
|
||||||
--button-intra-margin: $spacing-vertical;
|
--btn-padding: $spacing-vertical * 2/3;
|
||||||
--button-radius: 3px;
|
--btn-height: $spacing-vertical * 1.5;
|
||||||
|
--btn-intra-margin: $spacing-vertical;
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
--header-bg: var(--color-bg);
|
--header-bg: var(--color-bg);
|
||||||
--header-color: #666;
|
--header-color: #666;
|
||||||
--header-active-color: rgba(0, 0, 0, 0.85);
|
--header-active-color: rgba(0, 0, 0, 0.85);
|
||||||
--header-height: $spacing-vertical * 2.5;
|
--header-height: 65px;
|
||||||
--header-button-bg: transparent; //var(--button-bg);
|
--header-button-bg: transparent; //var(--button-bg);
|
||||||
--header-button-hover-bg: rgba(100, 100, 100, 0.15);
|
--header-button-hover-bg: rgba(100, 100, 100, 0.15);
|
||||||
|
|
||||||
|
@ -142,7 +144,6 @@ $text-color: #000;
|
||||||
--tooltip-width: 300px;
|
--tooltip-width: 300px;
|
||||||
--tooltip-bg: var(--color-bg);
|
--tooltip-bg: var(--color-bg);
|
||||||
--tooltip-color: var(--text-color);
|
--tooltip-color: var(--text-color);
|
||||||
--tooltip-border: 1px solid #aaa;
|
|
||||||
|
|
||||||
/* Scrollbar */
|
/* Scrollbar */
|
||||||
--scrollbar-radius: 10px;
|
--scrollbar-radius: 10px;
|
||||||
|
|
|
@ -1,89 +1,78 @@
|
||||||
@import '../mixin/link.scss';
|
/*
|
||||||
|
TODO:
|
||||||
|
Determine [disabled] or .disabled
|
||||||
|
Add <a> support (probably just get rid of button prefix)
|
||||||
|
*/
|
||||||
|
|
||||||
$button-focus-shift: 12%;
|
button {
|
||||||
|
border: none;
|
||||||
.button-set-item {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
+ .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);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border: 0 none;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: var(--button-radius);
|
|
||||||
text-transform: uppercase;
|
|
||||||
.icon {
|
|
||||||
top: 0em;
|
|
||||||
}
|
|
||||||
.icon:first-child {
|
|
||||||
padding-right: 5px;
|
|
||||||
}
|
|
||||||
.icon:last-child {
|
|
||||||
padding-left: 5px;
|
|
||||||
}
|
|
||||||
.icon:only-child {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.button-block {
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
position: relative;
|
||||||
font-size: 14px;
|
|
||||||
user-select: none;
|
|
||||||
transition: background var(--animation-duration) var(--animation-style);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button__content {
|
button:disabled.btn--disabled {
|
||||||
margin: 0 var(--button-padding);
|
cursor: default;
|
||||||
display: flex;
|
background-color: transparent;
|
||||||
.link-label {
|
|
||||||
text-decoration: none !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-primary {
|
button.btn {
|
||||||
color: var(--button-primary-color);
|
padding: 10px;
|
||||||
background-color: var(--button-primary-bg);
|
margin: 0 5px;
|
||||||
|
border-radius: var(--btn-radius);
|
||||||
|
color: var(--btn-primary-color);
|
||||||
|
background-color: var(--btn-primary-bg);
|
||||||
|
|
||||||
|
&:hover:not(.btn--disabled) {
|
||||||
box-shadow: var(--box-shadow-layer);
|
box-shadow: var(--box-shadow-layer);
|
||||||
|
|
||||||
&:focus {
|
|
||||||
//color: var(--button-primary-active-color);
|
|
||||||
//background-color:color: var(--button-primary-active-bg);
|
|
||||||
//box-shadow: $box-shadow-focus;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.button-alt {
|
|
||||||
background-color: var(--button-bg);
|
|
||||||
box-shadow: var(--box-shadow-layer);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-text {
|
button.btn.btn--alt {
|
||||||
@include text-link();
|
color: var(--btn-alt-color);
|
||||||
display: inline-block;
|
background-color: #efefef;
|
||||||
|
|
||||||
.button__content {
|
&:hover {
|
||||||
margin: 0 var(--text-link-padding);
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: #cdcdcd;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--color-help);
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.button-text-help {
|
|
||||||
@include text-link(var(--text-help-color));
|
button.btn.btn--circle {
|
||||||
font-size: 0.8em;
|
border-radius: 50%;
|
||||||
}
|
transition: all 0.2s;
|
||||||
.button--flat {
|
|
||||||
box-shadow: none !important;
|
&:hover:not([disabled]) {
|
||||||
|
border-radius: var(--btn-radius);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--submit {
|
button.btn.btn--inverse {
|
||||||
font-family: inherit;
|
box-shadow: none;
|
||||||
line-height: 0;
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,194 +2,19 @@
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
max-width: var(--card-max-width);
|
max-width: var(--card-max-width);
|
||||||
background: var(--card-bg);
|
|
||||||
box-shadow: var(--box-shadow-layer);
|
|
||||||
border-radius: var(--card-radius);
|
border-radius: var(--card-radius);
|
||||||
margin-bottom: var(--card-margin);
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
|
||||||
//below added to prevent scrollbar on long titles when show page loads, would prefer a cleaner CSS solution
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
.card--obscured {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.card--obscured .card__inner {
|
|
||||||
filter: blur(var(--nsfw-blur-intensity));
|
|
||||||
}
|
|
||||||
.card__title-primary,
|
|
||||||
.card__title-identity,
|
|
||||||
.card__content,
|
|
||||||
.card__subtext,
|
|
||||||
.card__actions {
|
|
||||||
padding: 0 var(--card-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card--small {
|
|
||||||
.card__title-primary,
|
|
||||||
.card__title-identity,
|
|
||||||
.card__actions,
|
|
||||||
.card__content,
|
|
||||||
.card__subtext {
|
|
||||||
padding: 0 calc(var(--card-padding) / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.card__title-primary {
|
|
||||||
margin-top: var(--card-margin);
|
|
||||||
margin-bottom: var(--card-margin);
|
|
||||||
}
|
|
||||||
.card__title-primary .meta {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.card__title-identity {
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
.card__actions {
|
|
||||||
margin-top: var(--card-margin);
|
|
||||||
margin-bottom: var(--card-margin);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.card__actions--bottom {
|
|
||||||
margin-top: $spacing-vertical * 1/3;
|
|
||||||
margin-bottom: $spacing-vertical * 1/3;
|
|
||||||
border-top: var(--divider);
|
|
||||||
}
|
|
||||||
.card__actions--form-submit {
|
|
||||||
margin-top: $spacing-vertical;
|
|
||||||
margin-bottom: var(--card-margin);
|
|
||||||
}
|
|
||||||
.card__action--right {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
.card__content {
|
|
||||||
margin-top: var(--card-margin);
|
|
||||||
margin-bottom: var(--card-margin);
|
|
||||||
table:not(:last-child) {
|
|
||||||
margin-bottom: var(--card-margin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__actions--only-vertical {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__content--extra-vertical-space {
|
|
||||||
margin: $spacing-vertical 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$font-size-subtext-multiple: 0.82;
|
|
||||||
.card__subtext {
|
|
||||||
color: var(--color-meta-light);
|
|
||||||
font-size: calc(var(--font-size-subtext-multiple) * 1em);
|
|
||||||
margin-top: $spacing-vertical * 1/3;
|
|
||||||
margin-bottom: $spacing-vertical * 1/3;
|
|
||||||
}
|
|
||||||
.card__subtext--allow-newlines {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
.card__subtext--two-lines {
|
|
||||||
height: calc(
|
|
||||||
var(--font-size) * var(--font-size-subtext-multiple) * var(--font-line-height) * 2
|
|
||||||
); /*this is so one line text still has the proper height*/
|
|
||||||
}
|
|
||||||
.card-overlay {
|
|
||||||
position: absolute;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
top: 0px;
|
|
||||||
bottom: 0px;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: var(--color-dark-overlay);
|
|
||||||
color: #fff;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__link {
|
.card--placeholder {
|
||||||
display: block;
|
background-color: black;
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.card--link {
|
|
||||||
transition: transform 0.2s var(--animation-style);
|
|
||||||
}
|
|
||||||
.card--link:hover {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
box-shadow: var(--box-shadow-focus);
|
|
||||||
transform: scale(var(--card-link-scaling)) translateX(var(--card-hover-translate));
|
|
||||||
transform-origin: 50% 50%;
|
|
||||||
overflow-x: visible;
|
|
||||||
overflow-y: visible;
|
|
||||||
}
|
|
||||||
.card--link:hover ~ .card--link {
|
|
||||||
transform: translateX(calc(var(--card-hover-translate) * 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__media {
|
|
||||||
background-size: cover;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: 50% 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__media--autothumb {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.card__media--autothumb.purple {
|
|
||||||
background-color: #9c27b0;
|
|
||||||
}
|
|
||||||
.card__media--autothumb.red {
|
|
||||||
background-color: #e53935;
|
|
||||||
}
|
|
||||||
.card__media--autothumb.pink {
|
|
||||||
background-color: #e91e63;
|
|
||||||
}
|
|
||||||
.card__media--autothumb.indigo {
|
|
||||||
background-color: #3f51b5;
|
|
||||||
}
|
|
||||||
.card__media--autothumb.blue {
|
|
||||||
background-color: #2196f3;
|
|
||||||
}
|
|
||||||
.card__media--autothumb.light-blue {
|
|
||||||
background-color: #039be5;
|
|
||||||
}
|
|
||||||
.card__media--autothumb.cyan {
|
|
||||||
background-color: #00acc1;
|
|
||||||
}
|
|
||||||
.card__media--autothumb.teal {
|
|
||||||
background-color: #009688;
|
|
||||||
}
|
|
||||||
.card__media--autothumb.green {
|
|
||||||
background-color: #43a047;
|
|
||||||
}
|
|
||||||
.card__media--autothumb.yellow {
|
|
||||||
background-color: #ffeb3b;
|
|
||||||
}
|
|
||||||
.card__media--autothumb.orange {
|
|
||||||
background-color: #ffa726;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__media--autothumb .card__autothumb__text {
|
|
||||||
font-size: 2em;
|
|
||||||
width: 100%;
|
|
||||||
color: #ffffff;
|
|
||||||
text-align: center;
|
|
||||||
position: absolute;
|
|
||||||
top: 36%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__indicators {
|
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card--small {
|
.card--small {
|
||||||
width: var(--card-small-width);
|
width: var(--card-small-width);
|
||||||
|
min-height: var(--card-small-width);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
@ -197,126 +22,237 @@ $font-size-subtext-multiple: 0.82;
|
||||||
height: calc(var(--card-small-width) * 9 / 16);
|
height: calc(var(--card-small-width) * 9 / 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card--form {
|
.card__link {
|
||||||
width: calc(var(--input-width) + var(--card-padding) * 2);
|
cursor: pointer;
|
||||||
|
|
||||||
|
// TODO: hover animations
|
||||||
|
// :hover {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__media {
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-color: var(--color-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__media--autothumb {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__title-identity {
|
||||||
|
margin-top: $spacing-vertical * 1/3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: regular .card__title for show page
|
||||||
|
.card__title--small {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__subtitle {
|
.card__subtitle {
|
||||||
color: var(--color-help);
|
color: var(--color-help);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
line-height: calc(var(--font-line-height) * 1 / 0.85);
|
padding-top: $spacing-vertical * 1/3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card--file-subtitle {
|
// .card__title-primary .meta {
|
||||||
display: flex;
|
// white-space: nowrap;
|
||||||
}
|
// overflow: hidden;
|
||||||
|
// text-overflow: ellipsis;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
|
||||||
// this is too specific
|
//
|
||||||
// it should be a helper class
|
// .card__actions {
|
||||||
// ex. ".m-padding-left"
|
// margin-top: var(--card-margin);
|
||||||
// will come back to this during the redesign - sean
|
// margin-bottom: var(--card-margin);
|
||||||
.card__publish-date {
|
// user-select: none;
|
||||||
padding-left: 20px;
|
// }
|
||||||
}
|
//
|
||||||
|
// .card__actions--bottom {
|
||||||
|
// margin-top: $spacing-vertical * 1/3;
|
||||||
|
// margin-bottom: $spacing-vertical * 1/3;
|
||||||
|
// border-top: var(--divider);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// .card__actions--form-submit {
|
||||||
|
// margin-top: $spacing-vertical;
|
||||||
|
// margin-bottom: var(--card-margin);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// .card__action--right {
|
||||||
|
// float: right;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// .card__content {
|
||||||
|
// margin-top: var(--card-margin);
|
||||||
|
// margin-bottom: var(--card-margin);
|
||||||
|
// table:not(:last-child) {
|
||||||
|
// margin-bottom: var(--card-margin);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// .card__actions--only-vertical {
|
||||||
|
// margin-left: 0;
|
||||||
|
// margin-right: 0;
|
||||||
|
// padding-left: 0;
|
||||||
|
// padding-right: 0;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// .card__content--extra-vertical-space {
|
||||||
|
// margin: $spacing-vertical 0;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $font-size-subtext-multiple: 0.82;
|
||||||
|
// .card__subtext {
|
||||||
|
// color: var(--color-meta-light);
|
||||||
|
// font-size: calc(var(--font-size-subtext-multiple) * 1em);
|
||||||
|
// margin-top: $spacing-vertical * 1/3;
|
||||||
|
// margin-bottom: $spacing-vertical * 1/3;
|
||||||
|
// }
|
||||||
|
// .card__subtext--allow-newlines {
|
||||||
|
// white-space: pre-wrap;
|
||||||
|
// }
|
||||||
|
// .card__subtext--two-lines {
|
||||||
|
// height: calc(
|
||||||
|
// var(--font-size) * var(--font-size-subtext-multiple) * var(--font-line-height) * 2
|
||||||
|
// ); /*this is so one line text still has the proper height*/
|
||||||
|
// }
|
||||||
|
// .card-overlay {
|
||||||
|
// position: absolute;
|
||||||
|
// left: 0px;
|
||||||
|
// right: 0px;
|
||||||
|
// top: 0px;
|
||||||
|
// bottom: 0px;
|
||||||
|
// padding: 20px;
|
||||||
|
// background-color: var(--color-dark-overlay);
|
||||||
|
// color: #fff;
|
||||||
|
// display: flex;
|
||||||
|
// align-items: center;
|
||||||
|
// font-weight: 600;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// .card__media--autothumb {
|
||||||
|
// position: relative;
|
||||||
|
// }
|
||||||
|
// .card__media--autothumb.purple {
|
||||||
|
// background-color: #9c27b0;
|
||||||
|
// }
|
||||||
|
// .card__media--autothumb.red {
|
||||||
|
// background-color: #e53935;
|
||||||
|
// }
|
||||||
|
// .card__media--autothumb.pink {
|
||||||
|
// background-color: #e91e63;
|
||||||
|
// }
|
||||||
|
// .card__media--autothumb.indigo {
|
||||||
|
// background-color: #3f51b5;
|
||||||
|
// }
|
||||||
|
// .card__media--autothumb.blue {
|
||||||
|
// background-color: #2196f3;
|
||||||
|
// }
|
||||||
|
// .card__media--autothumb.light-blue {
|
||||||
|
// background-color: #039be5;
|
||||||
|
// }
|
||||||
|
// .card__media--autothumb.cyan {
|
||||||
|
// background-color: #00acc1;
|
||||||
|
// }
|
||||||
|
// .card__media--autothumb.teal {
|
||||||
|
// background-color: #009688;
|
||||||
|
// }
|
||||||
|
// .card__media--autothumb.green {
|
||||||
|
// background-color: #43a047;
|
||||||
|
// }
|
||||||
|
// .card__media--autothumb.yellow {
|
||||||
|
// background-color: #ffeb3b;
|
||||||
|
// }
|
||||||
|
// .card__media--autothumb.orange {
|
||||||
|
// background-color: #ffa726;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// .card__media--autothumb .card__autothumb__text {
|
||||||
|
// font-size: 2em;
|
||||||
|
// width: 100%;
|
||||||
|
// color: #ffffff;
|
||||||
|
// text-align: center;
|
||||||
|
// position: absolute;
|
||||||
|
// top: 36%;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// .card--form {
|
||||||
|
// width: calc(var(--input-width) + var(--card-padding) * 2);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
|
||||||
.card-series-submit {
|
//
|
||||||
margin-left: auto;
|
// .card-series-submit {
|
||||||
margin-right: auto;
|
// margin-left: auto;
|
||||||
max-width: var(--card-max-width);
|
// margin-right: auto;
|
||||||
padding: $spacing-vertical / 2;
|
// max-width: var(--card-max-width);
|
||||||
}
|
// padding: $spacing-vertical / 2;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/*
|
||||||
|
.card-row is used on the discover page
|
||||||
|
It is a list of cards that extend past the right edge of the screen
|
||||||
|
There are left/right arrows to scroll the cards and view hidden content
|
||||||
|
*/
|
||||||
.card-row {
|
.card-row {
|
||||||
+ .card-row {
|
|
||||||
margin-top: $spacing-vertical * 1/3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-row__placeholder {
|
|
||||||
padding-bottom: $spacing-vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
$padding-top-card-hover-hack: 20px;
|
|
||||||
$padding-right-card-hover-hack: 30px;
|
|
||||||
|
|
||||||
.card-row__items {
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
/*hacky way to give space for hover */
|
|
||||||
padding-top: $padding-top-card-hover-hack;
|
|
||||||
margin-top: -1 * $padding-top-card-hover-hack;
|
|
||||||
padding-right: $padding-right-card-hover-hack;
|
|
||||||
margin-right: -1 * $padding-right-card-hover-hack;
|
|
||||||
> .card {
|
|
||||||
vertical-align: top;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
> .card + .card {
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-row--small {
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: var(--card-small-width);
|
min-width: var(--card-small-width);
|
||||||
margin-right: $spacing-vertical;
|
padding-top: $spacing-vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-row__header {
|
.card-row__header {
|
||||||
margin-bottom: 16px;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
// specific padding-left styling is needed here
|
||||||
|
// this needs to be used on a page with noPadding
|
||||||
|
// doing so allows the content to scroll to the edge of the screen
|
||||||
|
padding-left: $spacing-vertical * 2/3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-row__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-row__scrollhouse {
|
.card-row__scrollhouse {
|
||||||
position: relative;
|
padding-top: $spacing-vertical * 2/3;
|
||||||
/*hacky way to give space for hover */
|
overflow: hidden;
|
||||||
padding-right: $padding-right-card-hover-hack;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-row__nav {
|
.card {
|
||||||
position: absolute;
|
display: inline-block;
|
||||||
padding: 0 var(--card-margin);
|
vertical-align: top;
|
||||||
height: 100%;
|
margin-left: $spacing-vertical * 2/3;
|
||||||
top: calc($padding-top-card-hover-hack - var(--card-margin));
|
}
|
||||||
}
|
|
||||||
.card-row__nav .card-row__scroll-button {
|
.card:last-of-type {
|
||||||
background: var(--card-bg);
|
padding-right: $spacing-vertical * 2/3;
|
||||||
color: var(--color-help);
|
|
||||||
box-shadow: var(--box-shadow-layer);
|
|
||||||
padding: $spacing-vertical $spacing-vertical / 2;
|
|
||||||
position: absolute;
|
|
||||||
cursor: pointer;
|
|
||||||
left: 0;
|
|
||||||
top: 36%;
|
|
||||||
z-index: 2;
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: transform 0.2s var(--animation-style);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(calc(var(--card-link-scaling) * 1.1));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.card-row__nav--left {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
.card-row__nav--right {
|
|
||||||
right: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
if we keep doing things like this, we should add a real grid system, but I'm going to be a selective dick about it - Jeremy
|
if we keep doing things like this, we should add a real grid system, but I'm going to be a selective dick about it - Jeremy
|
||||||
*/
|
*/
|
||||||
.card-grid {
|
//TODO: css grid
|
||||||
$margin-card-grid: $spacing-vertical * 2/3;
|
// .card-grid {
|
||||||
display: flex;
|
// $margin-card-grid: $spacing-vertical * 2/3;
|
||||||
flex-wrap: wrap;
|
// display: flex;
|
||||||
> .card {
|
// flex-wrap: wrap;
|
||||||
width: $width-page-constrained / 2 - $margin-card-grid / 2;
|
// > .card {
|
||||||
flex-grow: 1;
|
// width: $width-page-constrained / 2 - $margin-card-grid / 2;
|
||||||
}
|
// flex-grow: 1;
|
||||||
> .card:nth-of-type(2n - 1):not(:last-child) {
|
// }
|
||||||
margin-right: $margin-card-grid;
|
// > .card:nth-of-type(2n - 1):not(:last-child) {
|
||||||
}
|
// margin-right: $margin-card-grid;
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
|
@ -5,12 +5,6 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this shouldn't know about the card width
|
|
||||||
// will come back to this for the redesign - sean
|
|
||||||
.channel-name--small {
|
|
||||||
width: calc(var(--card-small-width) * 2 / 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.channel-indicator__icon--invalid {
|
.channel-indicator__icon--invalid {
|
||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,64 +1,60 @@
|
||||||
#header {
|
#header {
|
||||||
color: var(--header-color);
|
|
||||||
background: var(--header-bg);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
box-shadow: var(--box-shadow-layer);
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: var(--header-height);
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
padding: $spacing-vertical / 2;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
color: var(--header-color);
|
||||||
.header__item {
|
background-color: var(--header-bg);
|
||||||
padding-left: $spacing-vertical / 4;
|
|
||||||
padding-right: $spacing-vertical / 4;
|
|
||||||
.button-alt {
|
|
||||||
background: var(--header-button-bg) !important;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
.button-alt:hover {
|
|
||||||
background: var(--header-button-hover-bg) !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header__item--wunderbar {
|
.header__actions-left {
|
||||||
flex-grow: 1;
|
display: flex;
|
||||||
|
padding: 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wunderbar {
|
.header__actions-right {
|
||||||
position: relative;
|
margin-left: auto;
|
||||||
.icon {
|
|
||||||
position: absolute;
|
|
||||||
left: 10px;
|
|
||||||
top: $spacing-vertical / 2 - 4px; //hacked
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wunderbar--active .icon-search {
|
.header__wunderbar {
|
||||||
color: var(--color-primary);
|
flex: 1;
|
||||||
|
max-width: 325px;
|
||||||
|
min-width: 175px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 5px;
|
||||||
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// below styles should be inside the common input styling
|
|
||||||
// will come back to this with the redesign - sean
|
|
||||||
.wunderbar__input {
|
.wunderbar__input {
|
||||||
background: var(--search-bg);
|
height: 50%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: var(--search-color);
|
color: var(--search-color);
|
||||||
height: $spacing-vertical * 1.5;
|
padding: 10px;
|
||||||
line-height: $spacing-vertical * 1.5;
|
background-color: #f3f3f3;
|
||||||
padding-left: 38px;
|
border-radius: 10px;
|
||||||
padding-right: 5px;
|
font-size: 0.9em;
|
||||||
border-radius: 2px;
|
|
||||||
border: var(--search-border);
|
|
||||||
transition: box-shadow var(--transition-duration) var(--transition-type);
|
|
||||||
&:focus {
|
&:focus {
|
||||||
background: var(--search-active-bg);
|
// TODO: focus style
|
||||||
color: var(--search-active-color);
|
|
||||||
box-shadow: var(--search-active-shadow);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wunderbar__suggestion {
|
||||||
|
padding: 5px;
|
||||||
|
background-color: var(--header-bg);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wunderbar__active-suggestion {
|
||||||
|
background-color: #a3ffb0;
|
||||||
|
}
|
||||||
|
|
|
@ -2,10 +2,7 @@
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
padding: 0 $spacing-vertical / 3;
|
||||||
|
|
||||||
.tooltip__link {
|
|
||||||
@include text-link();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip__body {
|
.tooltip__body {
|
||||||
|
@ -17,16 +14,15 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: $spacing-vertical / 2;
|
padding: $spacing-vertical / 2;
|
||||||
width: var(--tooltip-width);
|
width: var(--tooltip-width);
|
||||||
border: var(--tooltip-border);
|
|
||||||
color: var(--tooltip-color);
|
color: var(--tooltip-color);
|
||||||
background-color: var(--tooltip-bg);
|
background-color: var(--tooltip-bg);
|
||||||
font-size: calc(var(--font-size) * 7/8);
|
font-size: calc(var(--font-size) * 7/8);
|
||||||
line-height: var(--font-line-height);
|
line-height: var(--font-line-height);
|
||||||
box-shadow: var(--box-shadow-layer);
|
box-shadow: var(--box-shadow-layer);
|
||||||
|
border-radius: var(--card-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip--header .tooltip__link {
|
.tooltip__link {
|
||||||
@include text-link(#aaa);
|
|
||||||
font-size: calc(var(--font-size) * 3/4);
|
font-size: calc(var(--font-size) * 3/4);
|
||||||
margin-left: var(--button-padding);
|
margin-left: var(--button-padding);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
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:
|
dependencies:
|
||||||
utila "~0.3"
|
utila "~0.3"
|
||||||
|
|
||||||
|
dom-scroll-into-view@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/dom-scroll-into-view/-/dom-scroll-into-view-1.2.1.tgz#e8f36732dd089b0201a88d7815dc3f88e6d66c7e"
|
||||||
|
|
||||||
dom-serializer@0:
|
dom-serializer@0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
|
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
|
||||||
|
|
Loading…
Reference in a new issue