Seed Support #56
36 changed files with 962 additions and 924 deletions
|
@ -8,6 +8,7 @@ Web UI version numbers should always match the corresponding version of LBRY App
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
* The UI has been overhauled to use an omnibar and drop the sidebar.
|
||||
* The app is much more responsive switching pages. It no longer reloads the entire page and all assets on each page change.
|
||||
* lbry.js now offers a subscription model for wallet balance similar to file info.
|
||||
* Fixed file info subscribes not being unsubscribed in unmount.
|
||||
|
|
128
ui/js/app.js
128
ui/js/app.js
|
@ -12,10 +12,11 @@ import RewardPage from './page/reward.js';
|
|||
import WalletPage from './page/wallet.js';
|
||||
import ShowPage from './page/show.js';
|
||||
import PublishPage from './page/publish.js';
|
||||
import SearchPage from './page/search.js';
|
||||
import DiscoverPage from './page/discover.js';
|
||||
import DeveloperPage from './page/developer.js';
|
||||
import lbryuri from './lbryuri.js';
|
||||
import {FileListDownloaded, FileListPublished} from './page/file-list.js';
|
||||
import Drawer from './component/drawer.js';
|
||||
import Header from './component/header.js';
|
||||
import {Modal, ExpandableModal} from './component/modal.js';
|
||||
import {Link} from './component/link.js';
|
||||
|
@ -38,6 +39,7 @@ var App = React.createClass({
|
|||
data: 'Error data',
|
||||
},
|
||||
_fullScreenPages: ['watch'],
|
||||
_storeHistoryOfNextRender: false,
|
||||
|
||||
_upgradeDownloadItem: null,
|
||||
_isMounted: false,
|
||||
|
@ -73,15 +75,13 @@ var App = React.createClass({
|
|||
let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/);
|
||||
return {
|
||||
viewingPage: viewingPage,
|
||||
pageArgs: pageArgs === undefined ? null : pageArgs
|
||||
pageArgs: pageArgs === undefined ? null : decodeURIComponent(pageArgs)
|
||||
};
|
||||
},
|
||||
getInitialState: function() {
|
||||
var match, param, val, viewingPage, pageArgs,
|
||||
drawerOpenRaw = sessionStorage.getItem('drawerOpen');
|
||||
|
||||
return Object.assign(this.getViewingPageAndArgs(window.location.search), {
|
||||
drawerOpen: drawerOpenRaw !== null ? JSON.parse(drawerOpenRaw) : true,
|
||||
viewingPage: 'discover',
|
||||
appUrl: null,
|
||||
errorInfo: null,
|
||||
modal: null,
|
||||
downloadProgress: null,
|
||||
|
@ -89,6 +89,8 @@ var App = React.createClass({
|
|||
});
|
||||
},
|
||||
componentWillMount: function() {
|
||||
window.addEventListener("popstate", this.onHistoryPop);
|
||||
|
||||
document.addEventListener('unhandledError', (event) => {
|
||||
this.alertError(event.detail);
|
||||
});
|
||||
|
@ -105,9 +107,10 @@ var App = React.createClass({
|
|||
if (target.matches('a[href^="?"]')) {
|
||||
event.preventDefault();
|
||||
if (this._isMounted) {
|
||||
history.pushState({}, document.title, target.getAttribute('href'));
|
||||
this.registerHistoryPop();
|
||||
this.setState(this.getViewingPageAndArgs(target.getAttribute('href')));
|
||||
let appUrl = target.getAttribute('href');
|
||||
this._storeHistoryOfNextRender = true;
|
||||
this.setState(Object.assign({}, this.getViewingPageAndArgs(appUrl), { appUrl: appUrl }));
|
||||
document.body.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
target = target.parentNode;
|
||||
|
@ -125,14 +128,6 @@ var App = React.createClass({
|
|||
});
|
||||
}
|
||||
},
|
||||
openDrawer: function() {
|
||||
sessionStorage.setItem('drawerOpen', true);
|
||||
this.setState({ drawerOpen: true });
|
||||
},
|
||||
closeDrawer: function() {
|
||||
sessionStorage.setItem('drawerOpen', false);
|
||||
this.setState({ drawerOpen: false });
|
||||
},
|
||||
closeModal: function() {
|
||||
this.setState({
|
||||
modal: null,
|
||||
|
@ -143,12 +138,29 @@ var App = React.createClass({
|
|||
},
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
window.removeEventListener("popstate", this.onHistoryPop);
|
||||
},
|
||||
registerHistoryPop: function() {
|
||||
window.addEventListener("popstate", () => {
|
||||
this.setState(this.getViewingPageAndArgs(location.pathname));
|
||||
onHistoryPop: function() {
|
||||
this.setState(this.getViewingPageAndArgs(location.search));
|
||||
},
|
||||
onSearch: function(term) {
|
||||
this._storeHistoryOfNextRender = true;
|
||||
const isShow = term.startsWith('lbry://');
|
||||
this.setState({
|
||||
viewingPage: isShow ? "show" : "search",
|
||||
appUrl: (isShow ? "?show=" : "?search=") + encodeURIComponent(term),
|
||||
pageArgs: term
|
||||
});
|
||||
},
|
||||
onSubmit: function(uri) {
|
||||
this._storeHistoryOfNextRender = true;
|
||||
this.setState({
|
||||
address: uri,
|
||||
appUrl: "?show=" + encodeURIComponent(uri),
|
||||
viewingPage: "show",
|
||||
pageArgs: uri
|
||||
})
|
||||
},
|
||||
handleUpgradeClicked: function() {
|
||||
// Make a new directory within temp directory so the filename is guaranteed to be available
|
||||
const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep);
|
||||
|
@ -201,12 +213,6 @@ var App = React.createClass({
|
|||
modal: null,
|
||||
});
|
||||
},
|
||||
onSearch: function(term) {
|
||||
this.setState({
|
||||
viewingPage: 'discover',
|
||||
pageArgs: term
|
||||
});
|
||||
},
|
||||
alertError: function(error) {
|
||||
var errorInfoList = [];
|
||||
for (let key of Object.keys(error)) {
|
||||
|
@ -220,75 +226,57 @@ var App = React.createClass({
|
|||
errorInfo: <ul className="error-modal__error-list">{errorInfoList}</ul>,
|
||||
});
|
||||
},
|
||||
getHeaderLinks: function()
|
||||
{
|
||||
switch(this.state.viewingPage)
|
||||
{
|
||||
case 'wallet':
|
||||
case 'send':
|
||||
case 'receive':
|
||||
case 'rewards':
|
||||
return {
|
||||
'?wallet': 'Overview',
|
||||
'?send': 'Send',
|
||||
'?receive': 'Receive',
|
||||
'?rewards': 'Rewards',
|
||||
};
|
||||
case 'downloaded':
|
||||
case 'published':
|
||||
return {
|
||||
'?downloaded': 'Downloaded',
|
||||
'?published': 'Published',
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getMainContent: function()
|
||||
getContentAndAddress: function()
|
||||
{
|
||||
switch(this.state.viewingPage)
|
||||
{
|
||||
case 'search':
|
||||
return [this.state.pageArgs ? this.state.pageArgs : "Search", 'icon-search', <SearchPage query={this.state.pageArgs} />];
|
||||
case 'settings':
|
||||
return <SettingsPage />;
|
||||
return ["Settings", "icon-gear", <SettingsPage />];
|
||||
case 'help':
|
||||
return <HelpPage />;
|
||||
return ["Help", "icon-question", <HelpPage />];
|
||||
case 'report':
|
||||
return <ReportPage />;
|
||||
return ['Report an Issue', 'icon-file', <ReportPage />];
|
||||
case 'downloaded':
|
||||
return <FileListDownloaded />;
|
||||
return ["Downloads & Purchases", "icon-folder", <FileListDownloaded />];
|
||||
case 'published':
|
||||
return <FileListPublished />;
|
||||
return ["Publishes", "icon-folder", <FileListPublished />];
|
||||
case 'start':
|
||||
return <StartPage />;
|
||||
return ["Start", "icon-file", <StartPage />];
|
||||
case 'rewards':
|
||||
return <RewardsPage />;
|
||||
return ["Rewards", "icon-bank", <RewardsPage />];
|
||||
case 'wallet':
|
||||
case 'send':
|
||||
case 'receive':
|
||||
return <WalletPage viewingPage={this.state.viewingPage} />;
|
||||
return [this.state.viewingPage.charAt(0).toUpperCase() + this.state.viewingPage.slice(1), "icon-bank", <WalletPage viewingPage={this.state.viewingPage} />]
|
||||
case 'show':
|
||||
return <ShowPage uri={this.state.pageArgs} />;
|
||||
return [lbryuri.normalize(this.state.pageArgs), "icon-file", <ShowPage uri={this.state.pageArgs} />];
|
||||
case 'publish':
|
||||
return <PublishPage />;
|
||||
return ["Publish", "icon-upload", <PublishPage />];
|
||||
case 'developer':
|
||||
return <DeveloperPage />;
|
||||
return ["Developer", "icon-file", <DeveloperPage />];
|
||||
case 'discover':
|
||||
default:
|
||||
return <DiscoverPage showWelcome={this.state.justRegistered} {... this.state.pageArgs !== null ? {query: this.state.pageArgs} : {} } />;
|
||||
return ["Home", "icon-home", <DiscoverPage />];
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
var mainContent = this.getMainContent(),
|
||||
headerLinks = this.getHeaderLinks(),
|
||||
searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : '';
|
||||
let [address, wunderBarIcon, mainContent] = this.getContentAndAddress();
|
||||
|
||||
lbry.setTitle(address);
|
||||
|
||||
if (this._storeHistoryOfNextRender) {
|
||||
this._storeHistoryOfNextRender = false;
|
||||
history.pushState({}, document.title, this.state.appUrl);
|
||||
}
|
||||
|
||||
return (
|
||||
this._fullScreenPages.includes(this.state.viewingPage) ?
|
||||
mainContent :
|
||||
<div id="window" className={ this.state.drawerOpen ? 'drawer-open' : 'drawer-closed' }>
|
||||
<Drawer onCloseDrawer={this.closeDrawer} viewingPage={this.state.viewingPage} />
|
||||
<div id="main-content" className={ headerLinks ? 'with-sub-nav' : 'no-sub-nav' }>
|
||||
<Header onOpenDrawer={this.openDrawer} initialQuery={searchQuery} onSearch={this.onSearch} links={headerLinks} viewingPage={this.state.viewingPage} />
|
||||
<div id="window">
|
||||
<Header onSearch={this.onSearch} onSubmit={this.onSubmit} address={address} wunderBarIcon={wunderBarIcon} viewingPage={this.state.viewingPage} />
|
||||
<div id="main-content">
|
||||
{mainContent}
|
||||
</div>
|
||||
<Modal isOpen={this.state.modal == 'upgrade'} contentLabel="Update available"
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import $clamp from 'clamp-js-main';
|
||||
|
||||
//component/icon.js
|
||||
export let Icon = React.createClass({
|
||||
|
@ -19,29 +18,15 @@ export let Icon = React.createClass({
|
|||
|
||||
export let TruncatedText = React.createClass({
|
||||
propTypes: {
|
||||
lines: React.PropTypes.number,
|
||||
height: React.PropTypes.string,
|
||||
auto: React.PropTypes.bool,
|
||||
lines: React.PropTypes.number
|
||||
},
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
lines: null,
|
||||
height: null,
|
||||
auto: true,
|
||||
}
|
||||
},
|
||||
componentDidMount: function() {
|
||||
// Manually round up the line height, because clamp.js doesn't like fractional-pixel line heights.
|
||||
|
||||
// Need to work directly on the style object because setting the style prop doesn't update internal styles right away.
|
||||
this.refs.span.style.lineHeight = Math.ceil(parseFloat(getComputedStyle(this.refs.span).lineHeight)) + 'px';
|
||||
|
||||
$clamp(this.refs.span, {
|
||||
clamp: this.props.lines || this.props.height || 'auto',
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
return <span ref="span" className="truncated-text">{this.props.children}</span>;
|
||||
return <span className="truncated-text" style={{ WebkitLineClamp: this.props.lines }}>{this.props.children}</span>;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
import lbry from '../lbry.js';
|
||||
import React from 'react';
|
||||
import {Link} from './link.js';
|
||||
|
||||
var DrawerItem = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
subPages: [],
|
||||
};
|
||||
},
|
||||
render: function() {
|
||||
var isSelected = (this.props.viewingPage == this.props.href.substr(1) ||
|
||||
this.props.subPages.indexOf(this.props.viewingPage) != -1);
|
||||
return <Link {...this.props} className={ 'drawer-item ' + (isSelected ? 'drawer-item-selected' : '') } />
|
||||
}
|
||||
});
|
||||
|
||||
var drawerImageStyle = { //@TODO: remove this, img should be properly scaled once size is settled
|
||||
height: '36px'
|
||||
};
|
||||
|
||||
var Drawer = React.createClass({
|
||||
_balanceSubscribeId: null,
|
||||
|
||||
handleLogoClicked: function(event) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.shiftKey) {
|
||||
window.location.href = '?developer'
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
balance: 0,
|
||||
};
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
|
||||
this.setState({
|
||||
balance: balance
|
||||
});
|
||||
});
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
if (this._balanceSubscribeId) {
|
||||
lbry.balanceUnsubscribe(this._balanceSubscribeId)
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<nav id="drawer">
|
||||
<div id="drawer-handle">
|
||||
<Link title="Close" onClick={this.props.onCloseDrawer} icon="icon-bars" className="close-drawer-link"/>
|
||||
<a href="?discover" onMouseUp={this.handleLogoClicked}><img src={lbry.imagePath("lbry-dark-1600x528.png")} style={drawerImageStyle}/></a>
|
||||
</div>
|
||||
<DrawerItem href='?discover' viewingPage={this.props.viewingPage} label="Discover" icon="icon-search" />
|
||||
<DrawerItem href='?publish' viewingPage={this.props.viewingPage} label="Publish" icon="icon-upload" />
|
||||
<DrawerItem href='?downloaded' subPages={['published']} viewingPage={this.props.viewingPage} label="My Files" icon='icon-cloud-download' />
|
||||
<DrawerItem href="?wallet" subPages={['send', 'receive', 'rewards']} viewingPage={this.props.viewingPage} label="My Wallet" badge={lbry.formatCredits(this.state.balance) } icon="icon-bank" />
|
||||
<DrawerItem href='?settings' viewingPage={this.props.viewingPage} label="Settings" icon='icon-gear' />
|
||||
<DrawerItem href='?help' viewingPage={this.props.viewingPage} label="Help" icon='icon-question-circle' />
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default Drawer;
|
|
@ -3,7 +3,7 @@ import lbry from '../lbry.js';
|
|||
import lbryuri from '../lbryuri.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import {FileActions} from '../component/file-actions.js';
|
||||
import {Thumbnail, TruncatedText, FilePrice} from '../component/common.js';
|
||||
import {BusyMessage, TruncatedText, FilePrice} from '../component/common.js';
|
||||
import UriIndicator from '../component/channel-indicator.js';
|
||||
|
||||
/*should be merged into FileTile once FileTile is refactored to take a single id*/
|
||||
|
@ -77,40 +77,32 @@ export let FileTileStream = React.createClass({
|
|||
const isConfirmed = !!metadata;
|
||||
const title = isConfirmed ? metadata.title : uri;
|
||||
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
|
||||
const primaryUrl = "?show=" + uri;
|
||||
return (
|
||||
<section className={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
|
||||
<div className={"row-fluid card__inner file-tile__row"}>
|
||||
<div className="span3 file-tile__thumbnail-container">
|
||||
<a href={'?show=' + uri}><Thumbnail className="file-tile__thumbnail" {... metadata && metadata.thumbnail ? {src: metadata.thumbnail} : {}} alt={'Photo for ' + this.props.uri} /></a>
|
||||
<a href={primaryUrl} className="card__link">
|
||||
<div className={"card__inner file-tile__row"}>
|
||||
<div className="card__media"
|
||||
style={{ backgroundImage: "url('" + (metadata && metadata.thumbnail ? metadata.thumbnail : lbry.imagePath('default-thumb.svg')) + "')" }}>
|
||||
</div>
|
||||
<div className="span9">
|
||||
<div className="file-tile__content">
|
||||
<div className="card__title-primary">
|
||||
{ !this.props.hidePrice
|
||||
? <FilePrice uri={this.props.uri} />
|
||||
: null}
|
||||
<div className="meta"><a href={'?show=' + this.props.uri}>{uri}</a></div>
|
||||
<h3>
|
||||
<a href={'?show=' + uri} title={title}>
|
||||
<TruncatedText lines={1}>
|
||||
{title}
|
||||
</TruncatedText>
|
||||
</a>
|
||||
</h3>
|
||||
<div className="meta">{uri}</div>
|
||||
<h3><TruncatedText lines={1}>{title}</TruncatedText></h3>
|
||||
</div>
|
||||
<div className="card__actions">
|
||||
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this.props.contentType} />
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<p className="file-tile__description">
|
||||
<TruncatedText lines={2}>
|
||||
<div className="card__content card__subtext">
|
||||
<TruncatedText lines={3}>
|
||||
{isConfirmed
|
||||
? metadata.description
|
||||
: <span className="empty">This file is pending confirmation.</span>}
|
||||
</TruncatedText>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{this.state.showNsfwHelp
|
||||
? <div className='card-overlay'>
|
||||
<p>
|
||||
|
@ -227,6 +219,7 @@ export let FileCardStream = React.createClass({
|
|||
|
||||
export let FileTile = React.createClass({
|
||||
_isMounted: false,
|
||||
_isResolvePending: false,
|
||||
|
||||
propTypes: {
|
||||
uri: React.PropTypes.string.isRequired,
|
||||
|
@ -238,11 +231,10 @@ export let FileTile = React.createClass({
|
|||
claimInfo: null
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._isMounted = true;
|
||||
|
||||
lbry.resolve({uri: this.props.uri}).then((resolutionInfo) => {
|
||||
resolve: function(uri) {
|
||||
this._isResolvePending = true;
|
||||
lbry.resolve({uri: uri}).then((resolutionInfo) => {
|
||||
this._isResolvePending = false;
|
||||
if (this._isMounted && resolutionInfo && resolutionInfo.claim && resolutionInfo.claim.value &&
|
||||
resolutionInfo.claim.value.stream && resolutionInfo.claim.value.stream.metadata) {
|
||||
// In case of a failed lookup, metadata will be null, in which case the component will never display
|
||||
|
@ -252,6 +244,16 @@ export let FileTile = React.createClass({
|
|||
}
|
||||
});
|
||||
},
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.uri != this.props.uri) {
|
||||
this.setState(this.getInitialState());
|
||||
this.resolve(nextProps.uri);
|
||||
}
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this._isMounted = true;
|
||||
this.resolve(this.props.uri);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
},
|
||||
|
@ -261,6 +263,12 @@ export let FileTile = React.createClass({
|
|||
return <FileCardStream outpoint={null} metadata={{title: this.props.uri, description: "Loading..."}} contentType={null} hidePrice={true}
|
||||
hasSignature={false} signatureIsValid={false} uri={this.props.uri} />
|
||||
}
|
||||
if (this.props.showEmpty)
|
||||
{
|
||||
return this._isResolvePending ?
|
||||
<BusyMessage message="Loading magic decentralized data" /> :
|
||||
<div className="empty">{lbryuri.normalize(this.props.uri)} is unclaimed. <Link label="Put something here" href="?publish" /></div>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,76 +1,198 @@
|
|||
import React from 'react';
|
||||
import lbryuri from '../lbryuri.js';
|
||||
import {Link} from './link.js';
|
||||
import {Icon} from './common.js';
|
||||
import {Icon, CreditAmount} from './common.js';
|
||||
|
||||
var Header = React.createClass({
|
||||
_balanceSubscribeId: null,
|
||||
_isMounted: false,
|
||||
|
||||
propTypes: {
|
||||
onSearch: React.PropTypes.func.isRequired,
|
||||
onSubmit: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
title: "LBRY",
|
||||
isScrolled: false
|
||||
balance: 0
|
||||
};
|
||||
},
|
||||
componentWillMount: function() {
|
||||
new MutationObserver((mutations) => {
|
||||
this.setState({ title: mutations[0].target.textContent });
|
||||
}).observe(
|
||||
document.querySelector('title'),
|
||||
{ subtree: true, characterData: true, childList: true }
|
||||
);
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.addEventListener('scroll', this.handleScroll);
|
||||
this._isMounted = true;
|
||||
this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
|
||||
if (this._isMounted) {
|
||||
this.setState({balance: balance});
|
||||
}
|
||||
});
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
document.removeEventListener('scroll', this.handleScroll);
|
||||
if (this.userTypingTimer)
|
||||
{
|
||||
clearTimeout(this.userTypingTimer);
|
||||
this._isMounted = false;
|
||||
if (this._balanceSubscribeId) {
|
||||
lbry.balanceUnsubscribe(this._balanceSubscribeId)
|
||||
}
|
||||
},
|
||||
handleScroll: function() {
|
||||
this.setState({
|
||||
isScrolled: document.body.scrollTop > 0
|
||||
render: function() {
|
||||
return <header id="header">
|
||||
<div className="header__item">
|
||||
<Link onClick={() => { lbry.back() }} button="alt button--flat" icon="icon-arrow-left" />
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link href="?discover" button="alt button--flat" icon="icon-home" />
|
||||
</div>
|
||||
<div className="header__item header__item--wunderbar">
|
||||
<WunderBar address={this.props.address} icon={this.props.wunderBarIcon}
|
||||
onSearch={this.props.onSearch} onSubmit={this.props.onSubmit} viewingPage={this.props.viewingPage} />
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link href="?wallet" button="text" icon="icon-bank" label={lbry.formatCredits(this.state.balance, 1)} ></Link>
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link button="primary button--flat" href="?publish" icon="icon-upload" label="Publish" />
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link button="alt button--flat" href="?downloaded" icon="icon-folder" />
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link button="alt button--flat" href="?settings" icon="icon-gear" />
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
});
|
||||
},
|
||||
onQueryChange: function(event) {
|
||||
|
||||
if (this.userTypingTimer)
|
||||
{
|
||||
clearTimeout(this.userTypingTimer);
|
||||
class WunderBar extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onSearch: React.PropTypes.func.isRequired,
|
||||
onSubmit: React.PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
//@TODO: Switch to React.js timing
|
||||
var searchTerm = event.target.value;
|
||||
this.userTypingTimer = setTimeout(() => {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._userTypingTimer = null;
|
||||
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() {
|
||||
if (this.userTypingTimer) {
|
||||
clearTimeout(this._userTypingTimer);
|
||||
}
|
||||
}
|
||||
|
||||
onChange(event) {
|
||||
|
||||
if (this._userTypingTimer)
|
||||
{
|
||||
clearTimeout(this._userTypingTimer);
|
||||
}
|
||||
|
||||
this.setState({ address: event.target.value })
|
||||
|
||||
let searchTerm = event.target.value;
|
||||
|
||||
this._userTypingTimer = setTimeout(() => {
|
||||
this._resetOnNextBlur = false;
|
||||
this.props.onSearch(searchTerm);
|
||||
}, 800); // 800ms delay, tweak for faster/slower
|
||||
}
|
||||
|
||||
},
|
||||
render: function() {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.viewingPage !== this.props.viewingPage || nextProps.address != this.props.address) {
|
||||
this.setState({ address: nextProps.address, icon: nextProps.icon });
|
||||
}
|
||||
}
|
||||
|
||||
onFocus() {
|
||||
this._stateBeforeSearch = this.state;
|
||||
let 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() {
|
||||
let 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() {
|
||||
this._input.value = this.state.address;
|
||||
if (this._input && this._focusPending) {
|
||||
this._input.select();
|
||||
this._focusPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
onKeyPress(event) {
|
||||
if (event.charCode == 13 && this._input.value) {
|
||||
|
||||
let uri = null,
|
||||
method = "onSubmit";
|
||||
|
||||
this._resetOnNextBlur = false;
|
||||
clearTimeout(this._userTypingTimer);
|
||||
|
||||
try {
|
||||
uri = lbryuri.normalize(this._input.value);
|
||||
this.setState({ value: uri });
|
||||
} catch (error) { //then it's not a valid URL, so let's search
|
||||
uri = this._input.value;
|
||||
method = "onSearch";
|
||||
}
|
||||
|
||||
this.props[method](uri);
|
||||
this._input.blur();
|
||||
}
|
||||
}
|
||||
|
||||
onReceiveRef(ref) {
|
||||
this._input = ref;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<header id="header" className={ (this.state.isScrolled ? 'header-scrolled' : 'header-unscrolled') + ' ' + (this.props.links ? 'header-with-subnav' : 'header-no-subnav') }>
|
||||
<div className="header-top-bar">
|
||||
<Link onClick={this.props.onOpenDrawer} icon="icon-bars" className="open-drawer-link" />
|
||||
<h1>{ this.state.title }</h1>
|
||||
<div className="header-search">
|
||||
<Icon icon="icon-search" />
|
||||
<input type="search" onChange={this.onQueryChange} defaultValue={this.props.initialQuery}
|
||||
<div className={'wunderbar' + (this.state.isActive ? ' wunderbar--active' : '')}>
|
||||
{this.state.icon ? <Icon fixed icon={this.state.icon} /> : '' }
|
||||
<input className="wunderbar__input" type="search" placeholder="Type a LBRY address or search term"
|
||||
ref={this.onReceiveRef}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
onChange={this.onChange}
|
||||
onKeyPress={this.onKeyPress}
|
||||
value={this.state.address}
|
||||
placeholder="Find movies, music, games, and more" />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
this.props.links ?
|
||||
<SubHeader links={this.props.links} viewingPage={this.props.viewingPage} /> :
|
||||
''
|
||||
}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var SubHeader = React.createClass({
|
||||
export let SubHeader = React.createClass({
|
||||
render: function() {
|
||||
var links = [],
|
||||
let links = [],
|
||||
viewingUrl = '?' + this.props.viewingPage;
|
||||
|
||||
for (let link of Object.keys(this.props.links)) {
|
||||
|
@ -81,7 +203,7 @@ var SubHeader = React.createClass({
|
|||
);
|
||||
}
|
||||
return (
|
||||
<nav className="sub-header">
|
||||
<nav className={'sub-header' + (this.props.modifier ? ' sub-header--' + this.props.modifier : '')}>
|
||||
{links}
|
||||
</nav>
|
||||
);
|
||||
|
|
|
@ -41,7 +41,7 @@ export let Link = React.createClass({
|
|||
content = (
|
||||
<span {... 'button' in this.props ? {className: 'button__content'} : {}}>
|
||||
{'icon' in this.props ? <Icon icon={this.props.icon} fixed={true} /> : null}
|
||||
{<span className="link-label">{this.props.label}</span>}
|
||||
{this.props.label ? <span className="link-label">{this.props.label}</span> : null}
|
||||
{'badge' in this.props ? <span className="badge">{this.props.badge}</span> : null}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -9,9 +9,6 @@ var LoadScreen = React.createClass({
|
|||
details: React.PropTypes.string,
|
||||
isWarning: React.PropTypes.bool,
|
||||
},
|
||||
handleCancelClick: function() {
|
||||
history.back();
|
||||
},
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
isWarning: false,
|
||||
|
@ -34,9 +31,6 @@ var LoadScreen = React.createClass({
|
|||
<BusyMessage message={this.props.message} />
|
||||
</h3>
|
||||
{this.props.isWarning ? <Icon icon="icon-warning" /> : null} <span className={'load-screen__details ' + (this.props.isWarning ? 'load-screen__details--warning' : '')}>{this.props.details}</span>
|
||||
{window.history.length > 1
|
||||
? <div><Link label="Cancel" onClick={this.handleCancelClick} className='load-screen__cancel-link button-text' /></div>
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -31,7 +31,6 @@ function savePendingPublish({name, channel_name}) {
|
|||
return newPendingPublish;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If there is a pending publish with the given name or outpoint, remove it.
|
||||
* A channel name may also be provided along with name.
|
||||
|
@ -132,6 +131,21 @@ lbry.connect = function() {
|
|||
return lbry._connectPromise;
|
||||
}
|
||||
|
||||
|
||||
//kill this but still better than document.title =, which this replaced
|
||||
lbry.setTitle = function(title) {
|
||||
document.title = title + " - LBRY";
|
||||
}
|
||||
|
||||
//kill this with proper routing
|
||||
lbry.back = function() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
window.location.href = "?discover";
|
||||
}
|
||||
}
|
||||
|
||||
lbry.isDaemonAcceptingConnections = function (callback) {
|
||||
// Returns true/false whether the daemon is at a point it will start returning status
|
||||
lbry.call('status', {}, () => callback(true), null, () => callback(false))
|
||||
|
@ -633,7 +647,7 @@ lbry.resolve = function(params={}) {
|
|||
if (!params.uri) {
|
||||
throw "Resolve has hacked cache on top of it that requires a URI"
|
||||
}
|
||||
if (params.uri && claimCache[params.uri]) {
|
||||
if (params.uri && claimCache[params.uri] !== undefined) {
|
||||
resolve(claimCache[params.uri]);
|
||||
} else {
|
||||
lbry.call('resolve', params, function(data) {
|
||||
|
|
|
@ -7,7 +7,7 @@ const lbryio = {
|
|||
_accessToken: getLocal('accessToken'),
|
||||
_authenticationPromise: null,
|
||||
_user : null,
|
||||
enabled: true
|
||||
enabled: false
|
||||
};
|
||||
|
||||
const CONNECTION_STRING = process.env.LBRY_APP_API_URL ? process.env.LBRY_APP_API_URL : 'https://api.lbry.io/';
|
||||
|
@ -150,20 +150,6 @@ lbryio.authenticate = function() {
|
|||
} else {
|
||||
setCurrentUser()
|
||||
}
|
||||
// if (!lbryio._
|
||||
//(data) => {
|
||||
// resolve(data)
|
||||
// localStorage.setItem('accessToken', ID);
|
||||
// localStorage.setItem('appId', installation_id);
|
||||
// this.setState({
|
||||
// registrationCheckComplete: true,
|
||||
// justRegistered: true,
|
||||
// });
|
||||
//});
|
||||
// lbryio.call('user_install', 'exists', {app_id: installation_id}).then((userExists) => {
|
||||
// // TODO: deal with case where user exists already with the same app ID, but we have no access token.
|
||||
// // Possibly merge in to the existing user with the same app ID.
|
||||
// })
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,79 +1,18 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import lbryio from '../lbryio.js';
|
||||
import lbryuri from '../lbryuri.js';
|
||||
import lighthouse from '../lighthouse.js';
|
||||
import {FileTile, FileTileStream} from '../component/file-tile.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import {ToolTip} from '../component/tooltip.js';
|
||||
import {BusyMessage} from '../component/common.js';
|
||||
|
||||
var fetchResultsStyle = {
|
||||
color: '#888',
|
||||
textAlign: 'center',
|
||||
fontSize: '1.2em'
|
||||
};
|
||||
|
||||
var SearchActive = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div style={fetchResultsStyle}>
|
||||
<BusyMessage message="Looking up the Dewey Decimals" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var searchNoResultsStyle = {
|
||||
textAlign: 'center'
|
||||
}, searchNoResultsMessageStyle = {
|
||||
fontStyle: 'italic',
|
||||
marginRight: '5px'
|
||||
};
|
||||
|
||||
var SearchNoResults = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<section style={searchNoResultsStyle}>
|
||||
<span style={searchNoResultsMessageStyle}>No one has checked anything in for {this.props.query} yet.</span>
|
||||
<Link label="Be the first" href="?publish" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var SearchResults = React.createClass({
|
||||
render: function() {
|
||||
var rows = [],
|
||||
seenNames = {}; //fix this when the search API returns claim IDs
|
||||
|
||||
for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) {
|
||||
const uri = lbryuri.build({
|
||||
channelName: channel_name,
|
||||
contentName: name,
|
||||
claimId: channel_id || claim_id,
|
||||
});
|
||||
|
||||
rows.push(
|
||||
<FileTileStream key={name} uri={uri} outpoint={txid + ':' + nout} metadata={claim.stream.metadata} contentType={claim.stream.source.contentType} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>{rows}</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const communityCategoryToolTipText = ('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!');
|
||||
|
||||
var FeaturedCategory = React.createClass({
|
||||
let FeaturedCategory = React.createClass({
|
||||
render: function() {
|
||||
return (<div className="card-row card-row--small">
|
||||
{ this.props.category ?
|
||||
<h3 className="card-row__header">{this.props.category}
|
||||
{ this.props.category == "community" ?
|
||||
{ this.props.category.match(/^community/i) ?
|
||||
<ToolTip label="What's this?" body={communityCategoryToolTipText} className="tooltip--header"/>
|
||||
: '' }</h3>
|
||||
: '' }
|
||||
|
@ -82,7 +21,7 @@ var FeaturedCategory = React.createClass({
|
|||
}
|
||||
})
|
||||
|
||||
var FeaturedContent = React.createClass({
|
||||
let DiscoverPage = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
featuredUris: {},
|
||||
|
@ -105,7 +44,7 @@ var FeaturedContent = React.createClass({
|
|||
});
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
return <main>{
|
||||
this.state.failed ?
|
||||
<div className="empty">Failed to load landing content.</div> :
|
||||
<div>
|
||||
|
@ -117,89 +56,7 @@ var FeaturedContent = React.createClass({
|
|||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var DiscoverPage = React.createClass({
|
||||
userTypingTimer: null,
|
||||
|
||||
propTypes: {
|
||||
showWelcome: React.PropTypes.bool.isRequired,
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.props.query != this.state.query)
|
||||
{
|
||||
this.handleSearchChanged(this.props.query);
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
showWelcome: false,
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps, nextState) {
|
||||
if (nextProps.query != nextState.query)
|
||||
{
|
||||
this.handleSearchChanged(nextProps.query);
|
||||
}
|
||||
},
|
||||
|
||||
handleSearchChanged: function(query) {
|
||||
this.setState({
|
||||
searching: true,
|
||||
query: query,
|
||||
});
|
||||
|
||||
lighthouse.search(query).then(this.searchCallback);
|
||||
},
|
||||
|
||||
handleWelcomeDone: function() {
|
||||
this.setState({
|
||||
welcomeComplete: true,
|
||||
});
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
document.title = "Discover";
|
||||
|
||||
if (this.props.query) {
|
||||
// Rendering with a query already typed
|
||||
this.handleSearchChanged(this.props.query);
|
||||
}
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
welcomeComplete: false,
|
||||
results: [],
|
||||
query: this.props.query,
|
||||
searching: ('query' in this.props) && (this.props.query.length > 0)
|
||||
};
|
||||
},
|
||||
|
||||
searchCallback: function(results) {
|
||||
if (this.state.searching) //could have canceled while results were pending, in which case nothing to do
|
||||
{
|
||||
this.setState({
|
||||
results: results,
|
||||
searching: false //multiple searches can be out, we're only done if we receive one we actually care about
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<main>
|
||||
{ this.state.searching ? <SearchActive /> : null }
|
||||
{ !this.state.searching && this.props.query && this.state.results.length ? <SearchResults results={this.state.results} /> : null }
|
||||
{ !this.state.searching && this.props.query && !this.state.results.length ? <SearchNoResults query={this.props.query} /> : null }
|
||||
{ !this.props.query && !this.state.searching ? <FeaturedContent /> : null }
|
||||
</main>
|
||||
);
|
||||
}</main>;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -3,12 +3,22 @@ import lbry from '../lbry.js';
|
|||
import lbryuri from '../lbryuri.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import {FormField} from '../component/form.js';
|
||||
import {SubHeader} from '../component/header.js';
|
||||
import {FileTileStream} from '../component/file-tile.js';
|
||||
import rewards from '../rewards.js';
|
||||
import lbryio from '../lbryio.js';
|
||||
import {BusyMessage, Thumbnail} from '../component/common.js';
|
||||
|
||||
|
||||
export let FileListNav = React.createClass({
|
||||
render: function() {
|
||||
return <SubHeader modifier="constrained" viewingPage={this.props.viewingPage} links={{
|
||||
'?downloaded': 'Downloaded',
|
||||
'?published': 'Published',
|
||||
}} />;
|
||||
}
|
||||
});
|
||||
|
||||
export let FileListDownloaded = React.createClass({
|
||||
_isMounted: false,
|
||||
|
||||
|
@ -19,7 +29,6 @@ export let FileListDownloaded = React.createClass({
|
|||
},
|
||||
componentDidMount: function() {
|
||||
this._isMounted = true;
|
||||
document.title = "Downloaded Files";
|
||||
|
||||
lbry.claim_list_mine().then((myClaimInfos) => {
|
||||
if (!this._isMounted) { return; }
|
||||
|
@ -38,26 +47,21 @@ export let FileListDownloaded = React.createClass({
|
|||
this._isMounted = false;
|
||||
},
|
||||
render: function() {
|
||||
let content = "";
|
||||
if (this.state.fileInfos === null) {
|
||||
return (
|
||||
<main className="page">
|
||||
<BusyMessage message="Loading" />
|
||||
</main>
|
||||
);
|
||||
content = <BusyMessage message="Loading" />;
|
||||
} else if (!this.state.fileInfos.length) {
|
||||
return (
|
||||
<main className="page">
|
||||
<span>You haven't downloaded anything from LBRY yet. Go <Link href="?discover" label="search for your first download" />!</span>
|
||||
</main>
|
||||
);
|
||||
content = <span>You haven't downloaded anything from LBRY yet. Go <Link href="?discover" label="search for your first download" />!</span>;
|
||||
} else {
|
||||
content = <FileList fileInfos={this.state.fileInfos} hidePrices={true} />;
|
||||
}
|
||||
return (
|
||||
<main className="page">
|
||||
<FileList fileInfos={this.state.fileInfos} hidePrices={true} />
|
||||
<main className="main--single-column">
|
||||
<FileListNav viewingPage="downloaded" />
|
||||
{content}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export let FileListPublished = React.createClass({
|
||||
|
@ -79,12 +83,11 @@ export let FileListPublished = React.createClass({
|
|||
else {
|
||||
rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
|
||||
}
|
||||
});
|
||||
}, () => {});
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this._isMounted = true;
|
||||
this._requestPublishReward();
|
||||
document.title = "Published Files";
|
||||
|
||||
lbry.claim_list_mine().then((claimInfos) => {
|
||||
if (!this._isMounted) { return; }
|
||||
|
@ -103,28 +106,23 @@ export let FileListPublished = React.createClass({
|
|||
this._isMounted = false;
|
||||
},
|
||||
render: function () {
|
||||
let content = null;
|
||||
if (this.state.fileInfos === null) {
|
||||
return (
|
||||
<main className="page">
|
||||
<BusyMessage message="Loading" />
|
||||
</main>
|
||||
);
|
||||
content = <BusyMessage message="Loading" />;
|
||||
}
|
||||
else if (!this.state.fileInfos.length) {
|
||||
return (
|
||||
<main className="page">
|
||||
<span>You haven't published anything to LBRY yet.</span> Try <Link href="?publish" label="publishing" />!
|
||||
</main>
|
||||
);
|
||||
content = <span>You haven't published anything to LBRY yet. Try <Link href="?publish" label="publishing" />!</span>;
|
||||
}
|
||||
else {
|
||||
content = <FileList fileInfos={this.state.fileInfos} />;
|
||||
}
|
||||
return (
|
||||
<main className="page">
|
||||
<FileList fileInfos={this.state.fileInfos} />
|
||||
<main className="main--single-column">
|
||||
<FileListNav viewingPage="published" />
|
||||
{content}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export let FileList = React.createClass({
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import {SettingsNav} from './settings.js';
|
||||
import {version as uiVersion} from 'json!../../package.json';
|
||||
|
||||
var HelpPage = React.createClass({
|
||||
|
@ -24,9 +25,6 @@ var HelpPage = React.createClass({
|
|||
});
|
||||
});
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.title = "Help";
|
||||
},
|
||||
render: function() {
|
||||
let ver, osName, platform, newVerLink;
|
||||
if (this.state.versionInfo) {
|
||||
|
@ -49,7 +47,8 @@ var HelpPage = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<main className="page">
|
||||
<main className="main--single-column">
|
||||
<SettingsNav viewingPage="help" />
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<h3>Read the FAQ</h3>
|
||||
|
|
|
@ -148,7 +148,7 @@ var PublishPage = React.createClass({
|
|||
});
|
||||
},
|
||||
handlePublishStartedConfirmed: function() {
|
||||
window.location = "?published";
|
||||
window.location.href = "?published";
|
||||
},
|
||||
handlePublishError: function(error) {
|
||||
this.setState({
|
||||
|
@ -348,9 +348,6 @@ var PublishPage = React.createClass({
|
|||
componentWillMount: function() {
|
||||
this._updateChannelList();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.title = "Publish";
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
},
|
||||
onFileChange: function() {
|
||||
|
@ -387,7 +384,7 @@ var PublishPage = React.createClass({
|
|||
const lbcInputHelp = "This LBC remains yours and the deposit can be undone at any time."
|
||||
|
||||
return (
|
||||
<main ref="page">
|
||||
<main className="main--single-column">
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
|
@ -551,7 +548,7 @@ var PublishPage = React.createClass({
|
|||
|
||||
<div className="card-series-submit">
|
||||
<Link button="primary" label={!this.state.submitting ? 'Publish' : 'Publishing...'} onClick={this.handleSubmit} disabled={this.state.submitting} />
|
||||
<Link button="cancel" onClick={window.history.back} label="Cancel" />
|
||||
<Link button="cancel" onClick={lbry.back} label="Cancel" />
|
||||
<input type="submit" className="hidden" />
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -18,9 +18,6 @@ var ReportPage = React.createClass({
|
|||
this._messageArea.value = '';
|
||||
}
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.title = "Report an Issue";
|
||||
},
|
||||
closeModal: function() {
|
||||
this.setState({
|
||||
modal: null,
|
||||
|
@ -34,7 +31,7 @@ var ReportPage = React.createClass({
|
|||
},
|
||||
render: function() {
|
||||
return (
|
||||
<main className="page">
|
||||
<main className="main--single-column">
|
||||
<section className="card">
|
||||
<h3>Report an Issue</h3>
|
||||
<p>Please describe the problem you experienced and any information you think might be useful to us. Links to screenshots are great!</p>
|
||||
|
|
|
@ -4,6 +4,7 @@ import lbryio from '../lbryio.js';
|
|||
import {CreditAmount, Icon} from '../component/common.js';
|
||||
import rewards from '../rewards.js';
|
||||
import Modal from '../component/modal.js';
|
||||
import {WalletNav} from './wallet.js';
|
||||
import {RewardLink} from '../component/link.js';
|
||||
|
||||
const RewardTile = React.createClass({
|
||||
|
@ -56,14 +57,15 @@ var RewardsPage = React.createClass({
|
|||
},
|
||||
render: function() {
|
||||
return (
|
||||
<main>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<main className="main--single-column">
|
||||
<WalletNav viewingPage="rewards"/>
|
||||
<div>
|
||||
{!this.state.userRewards
|
||||
? (this.state.failed ? <div className="empty">Failed to load rewards.</div> : '')
|
||||
: this.state.userRewards.map(({RewardType, RewardTitle, RewardDescription, TransactionID, RewardAmount}) => {
|
||||
return <RewardTile key={RewardType} onRewardClaim={this.loadRewards} type={RewardType} title={RewardTitle} description={RewardDescription} claimed={!!TransactionID} value={RewardAmount} />;
|
||||
})}
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
165
ui/js/page/search.js
Normal file
165
ui/js/page/search.js
Normal file
|
@ -0,0 +1,165 @@
|
|||
import React from 'react';
|
||||
import lbry from '../lbry.js';
|
||||
import lbryio from '../lbryio.js';
|
||||
import lbryuri from '../lbryuri.js';
|
||||
import lighthouse from '../lighthouse.js';
|
||||
import {FileTile, FileTileStream} from '../component/file-tile.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import {ToolTip} from '../component/tooltip.js';
|
||||
import {BusyMessage} from '../component/common.js';
|
||||
|
||||
var SearchNoResults = React.createClass({
|
||||
render: function() {
|
||||
return <section>
|
||||
<span className="empty">
|
||||
No one has checked anything in for {this.props.query} yet.
|
||||
<Link label="Be the first" href="?publish" />
|
||||
</span>
|
||||
</section>;
|
||||
}
|
||||
});
|
||||
|
||||
var SearchResultList = React.createClass({
|
||||
render: function() {
|
||||
var rows = [],
|
||||
seenNames = {}; //fix this when the search API returns claim IDs
|
||||
|
||||
for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) {
|
||||
const uri = lbryuri.build({
|
||||
channelName: channel_name,
|
||||
contentName: name,
|
||||
claimId: channel_id || claim_id,
|
||||
});
|
||||
|
||||
rows.push(
|
||||
<FileTileStream key={uri} uri={uri} outpoint={txid + ':' + nout} metadata={claim.stream.metadata} contentType={claim.stream.source.contentType} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>{rows}</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let SearchResults = React.createClass({
|
||||
propTypes: {
|
||||
query: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
_isMounted: false,
|
||||
|
||||
search: function(term) {
|
||||
lighthouse.search(term).then(this.searchCallback);
|
||||
if (!this.state.searching) {
|
||||
this.setState({ searching: true })
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function () {
|
||||
this._isMounted = true;
|
||||
this.search(this.props.query);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
if (nextProps.query != this.props.query) {
|
||||
this.search(nextProps.query);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function () {
|
||||
this._isMounted = false;
|
||||
},
|
||||
|
||||
getInitialState: function () {
|
||||
return {
|
||||
results: [],
|
||||
searching: true
|
||||
};
|
||||
},
|
||||
|
||||
searchCallback: function (results) {
|
||||
if (this._isMounted) //could have canceled while results were pending, in which case nothing to do
|
||||
{
|
||||
this.setState({
|
||||
results: results,
|
||||
searching: false //multiple searches can be out, we're only done if we receive one we actually care about
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function () {
|
||||
return this.state.searching ?
|
||||
<BusyMessage message="Looking up the Dewey Decimals" /> :
|
||||
(this.state.results && this.state.results.length ?
|
||||
<SearchResultList results={this.state.results} /> :
|
||||
<SearchNoResults query={this.props.query} />);
|
||||
}
|
||||
});
|
||||
|
||||
let SearchPage = React.createClass({
|
||||
|
||||
_isMounted: false,
|
||||
|
||||
propTypes: {
|
||||
query: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
isValidUri: function(query) {
|
||||
try {
|
||||
lbryuri.parse(query);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._isMounted = true;
|
||||
lighthouse.search(this.props.query).then(this.searchCallback);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
results: [],
|
||||
searching: true
|
||||
};
|
||||
},
|
||||
|
||||
searchCallback: function(results) {
|
||||
if (this._isMounted) //could have canceled while results were pending, in which case nothing to do
|
||||
{
|
||||
this.setState({
|
||||
results: results,
|
||||
searching: false //multiple searches can be out, we're only done if we receive one we actually care about
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<main className="main--single-column">
|
||||
{ this.isValidUri(this.props.query) ?
|
||||
<section className="section-spaced">
|
||||
<h3 className="card-row__header">
|
||||
Exact URL
|
||||
<ToolTip label="?" body="This is the resolution of a LBRY URL and not controlled by LBRY Inc." className="tooltip--header"/>
|
||||
</h3>
|
||||
<FileTile uri={this.props.query} showEmpty={true} />
|
||||
</section> : '' }
|
||||
<section className="section-spaced">
|
||||
<h3 className="card-row__header">
|
||||
Search Results for {this.props.query}
|
||||
<ToolTip label="?" body="These search results are provided by LBRY Inc." className="tooltip--header"/>
|
||||
</h3>
|
||||
<SearchResults query={this.props.query} />
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default SearchPage;
|
|
@ -1,7 +1,17 @@
|
|||
import React from 'react';
|
||||
import {FormField, FormRow} from '../component/form.js';
|
||||
import {SubHeader} from '../component/header.js';
|
||||
import lbry from '../lbry.js';
|
||||
|
||||
export let SettingsNav = React.createClass({
|
||||
render: function() {
|
||||
return <SubHeader modifier="constrained" viewingPage={this.props.viewingPage} links={{
|
||||
'?settings': 'Settings',
|
||||
'?help' : 'Help'
|
||||
}} />;
|
||||
}
|
||||
});
|
||||
|
||||
var SettingsPage = React.createClass({
|
||||
_onSettingSaveSuccess: function() {
|
||||
// This is bad.
|
||||
|
@ -56,9 +66,6 @@ var SettingsPage = React.createClass({
|
|||
showUnavailable: lbry.getClientSetting('showUnavailable'),
|
||||
}
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.title = "Settings";
|
||||
},
|
||||
componentWillMount: function() {
|
||||
lbry.getDaemonSettings((settings) => {
|
||||
this.setState({
|
||||
|
@ -92,7 +99,8 @@ var SettingsPage = React.createClass({
|
|||
</section>
|
||||
*/
|
||||
return (
|
||||
<main>
|
||||
<main className="main--single-column">
|
||||
<SettingsNav viewingPage="settings" />
|
||||
<section className="card">
|
||||
<div className="card__content">
|
||||
<h3>Download Directory</h3>
|
||||
|
|
|
@ -16,8 +16,11 @@ var FormatItem = React.createClass({
|
|||
outpoint: React.PropTypes.string,
|
||||
},
|
||||
render: function() {
|
||||
const {thumbnail, author, title, description, language, license} = this.props.metadata;
|
||||
const mediaType = lbry.getMediaType(this.props.contentType);
|
||||
const {author, language, license} = this.props.metadata;
|
||||
|
||||
if (!this.props.contentType && [author, language, license].filter((val) => {return !!val; }).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="table-standard">
|
||||
|
@ -40,92 +43,107 @@ var FormatItem = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
let ShowPage = React.createClass({
|
||||
_uri: null,
|
||||
let ChannelPage = React.createClass({
|
||||
render: function() {
|
||||
return <main className="main--single-column">
|
||||
<section className="card">
|
||||
<div className="card__inner">
|
||||
<div className="card__title-identity"><h1>{this.props.title}</h1></div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<p>
|
||||
This channel page is a stub.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
}
|
||||
});
|
||||
|
||||
let FilePage = React.createClass({
|
||||
_isMounted: false,
|
||||
|
||||
propTypes: {
|
||||
uri: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
metadata: null,
|
||||
contentType: null,
|
||||
hasSignature: false,
|
||||
signatureIsValid: false,
|
||||
cost: null,
|
||||
costIncludesData: null,
|
||||
uriLookupComplete: null,
|
||||
isDownloaded: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.outpoint != this.props.outpoint || nextProps.uri != this.props.uri) {
|
||||
this.loadCostAndFileState(nextProps.uri, nextProps.outpoint);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._uri = lbryuri.normalize(this.props.uri);
|
||||
document.title = this._uri;
|
||||
this._isMounted = true;
|
||||
this.loadCostAndFileState(this.props.uri, this.props.outpoint);
|
||||
},
|
||||
|
||||
lbry.resolve({uri: this._uri}).then(({ claim: {txid, nout, has_signature, signature_is_valid, value: {stream: {metadata, source: {contentType}}}}}) => {
|
||||
const outpoint = txid + ':' + nout;
|
||||
|
||||
lbry.file_list({outpoint}).then((fileInfo) => {
|
||||
loadCostAndFileState: function(uri, outpoint) {
|
||||
lbry.file_list({outpoint: outpoint}).then((fileInfo) => {
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
isDownloaded: fileInfo.length > 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
outpoint: outpoint,
|
||||
metadata: metadata,
|
||||
hasSignature: has_signature,
|
||||
signatureIsValid: signature_is_valid,
|
||||
contentType: contentType,
|
||||
uriLookupComplete: true,
|
||||
});
|
||||
});
|
||||
|
||||
lbry.getCostInfo(this._uri).then(({cost, includesData}) => {
|
||||
lbry.getCostInfo(uri).then(({cost, includesData}) => {
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
cost: cost,
|
||||
costIncludesData: includesData,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const metadata = this.state.metadata;
|
||||
const title = metadata ? this.state.metadata.title : this._uri;
|
||||
const metadata = this.props.metadata,
|
||||
title = metadata ? this.props.metadata.title : this.props.uri,
|
||||
uriIndicator = <UriIndicator uri={this.props.uri} hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} />;
|
||||
|
||||
return (
|
||||
<main className="constrained-page">
|
||||
<main className="main--single-column">
|
||||
<section className="show-page-media">
|
||||
{ this.state.contentType && this.state.contentType.startsWith('video/') ?
|
||||
<Video className="video-embedded" uri={this._uri} metadata={metadata} outpoint={this.state.outpoint} /> :
|
||||
{ this.props.contentType && this.props.contentType.startsWith('video/') ?
|
||||
<Video className="video-embedded" uri={this.props.uri} metadata={metadata} outpoint={this.props.outpoint} /> :
|
||||
(metadata ? <Thumbnail src={metadata.thumbnail} /> : <Thumbnail />) }
|
||||
</section>
|
||||
<section className="card">
|
||||
<div className="card__inner">
|
||||
<div className="card__title-identity">
|
||||
{this.state.isDownloaded === false
|
||||
? <span style={{float: "right"}}><FilePrice uri={this._uri} metadata={this.state.metadata} /></span>
|
||||
? <span style={{float: "right"}}><FilePrice uri={this.props.uri} metadata={metadata} /></span>
|
||||
: null}
|
||||
<h1>{title}</h1>
|
||||
{ this.state.uriLookupComplete ?
|
||||
<div>
|
||||
<div className="card__subtitle">
|
||||
<UriIndicator uri={this._uri} hasSignature={this.state.hasSignature} signatureIsValid={this.state.signatureIsValid} />
|
||||
{ this.props.channelUri ?
|
||||
<Link href={"?show=" + this.props.channelUri }>{uriIndicator}</Link> :
|
||||
uriIndicator}
|
||||
</div>
|
||||
<div className="card__actions">
|
||||
<FileActions uri={this._uri} outpoint={this.state.outpoint} metadata={metadata} contentType={this.state.contentType} />
|
||||
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this.props.contentType} />
|
||||
</div>
|
||||
</div> : '' }
|
||||
</div>
|
||||
{ this.state.uriLookupComplete ?
|
||||
<div>
|
||||
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
|
||||
{metadata.description}
|
||||
</div>
|
||||
</div>
|
||||
: <div className="card__content"><BusyMessage message="Loading magic decentralized data..." /></div> }
|
||||
</div>
|
||||
{ metadata ?
|
||||
<div className="card__content">
|
||||
<FormatItem metadata={metadata} contentType={this.state.contentType} cost={this.state.cost} uri={this._uri} outpoint={this.state.outpoint} costIncludesData={this.state.costIncludesData} />
|
||||
<FormatItem metadata={metadata} contentType={this.state.contentType} cost={this.state.cost} uri={this.props.uri} outpoint={this.props.outpoint} costIncludesData={this.state.costIncludesData} />
|
||||
</div> : '' }
|
||||
<div className="card__content">
|
||||
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
|
||||
|
@ -136,4 +154,131 @@ let ShowPage = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
let ShowPage = React.createClass({
|
||||
_uri: null,
|
||||
_isMounted: false,
|
||||
|
||||
propTypes: {
|
||||
uri: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
outpoint: null,
|
||||
metadata: null,
|
||||
contentType: null,
|
||||
hasSignature: false,
|
||||
claimType: null,
|
||||
signatureIsValid: false,
|
||||
cost: null,
|
||||
costIncludesData: null,
|
||||
uriLookupComplete: null,
|
||||
isFailed: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.uri != this.props.uri) {
|
||||
this.setState(this.getInitialState());
|
||||
this.loadUri(nextProps.uri);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._isMounted = true;
|
||||
this.loadUri(this.props.uri);
|
||||
},
|
||||
|
||||
loadUri: function(uri) {
|
||||
this._uri = lbryuri.normalize(uri);
|
||||
|
||||
lbry.resolve({uri: this._uri}).then((resolveData) => {
|
||||
const isChannel = resolveData && resolveData.claims_in_channel;
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
if (resolveData) {
|
||||
let newState = { uriLookupComplete: true }
|
||||
if (!isChannel) {
|
||||
let {claim: {txid: txid, nout: nout, has_signature: has_signature, signature_is_valid: signature_is_valid, value: {stream: {metadata: metadata, source: {contentType: contentType}}}}} = resolveData;
|
||||
|
||||
Object.assign(newState, {
|
||||
claimType: "file",
|
||||
metadata: metadata,
|
||||
outpoint: txid + ':' + nout,
|
||||
hasSignature: has_signature,
|
||||
signatureIsValid: signature_is_valid,
|
||||
contentType: contentType
|
||||
});
|
||||
|
||||
|
||||
lbry.setTitle(metadata.title ? metadata.title : this._uri)
|
||||
|
||||
} else {
|
||||
let {certificate: {txid: txid, nout: nout, has_signature: has_signature}} = resolveData;
|
||||
Object.assign(newState, {
|
||||
claimType: "channel",
|
||||
outpoint: txid + ':' + nout,
|
||||
txid: txid,
|
||||
metadata: {
|
||||
title:resolveData.certificate.name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
|
||||
} else {
|
||||
this.setState(Object.assign({}, this.getInitialState(), {
|
||||
uriLookupComplete: true,
|
||||
isFailed: true
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const metadata = this.state.metadata,
|
||||
title = metadata ? this.state.metadata.title : this._uri;
|
||||
|
||||
let innerContent = "";
|
||||
|
||||
if (!this.state.uriLookupComplete || this.state.isFailed) {
|
||||
innerContent = <section className="card">
|
||||
<div className="card__inner">
|
||||
<div className="card__title-identity"><h1>{title}</h1></div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
{ this.state.uriLookupComplete ?
|
||||
<p>This location is not yet in use. { ' ' }<Link href="?publish" label="Put something here" />.</p> :
|
||||
<BusyMessage message="Loading magic decentralized data..." />
|
||||
}
|
||||
</div>
|
||||
</section>;
|
||||
} else if (this.state.claimType == "channel") {
|
||||
innerContent = <ChannelPage title={this._uri} />
|
||||
} else {
|
||||
let channelUriObj = lbryuri.parse(this._uri)
|
||||
delete channelUriObj.path;
|
||||
delete channelUriObj.contentName;
|
||||
const channelUri = this.state.signatureIsValid && this.state.hasSignature && channelUriObj.isChannel ? lbryuri.build(channelUriObj, false) : null;
|
||||
innerContent = <FilePage
|
||||
uri={this._uri}
|
||||
channelUri={channelUri}
|
||||
outpoint={this.state.outpoint}
|
||||
metadata={metadata}
|
||||
contentType={this.state.contentType}
|
||||
hasSignature={this.state.hasSignature}
|
||||
signatureIsValid={this.state.signatureIsValid}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <main className="main--single-column">{innerContent}</main>;
|
||||
}
|
||||
});
|
||||
|
||||
export default ShowPage;
|
||||
|
|
|
@ -5,12 +5,9 @@ var StartPage = React.createClass({
|
|||
componentWillMount: function() {
|
||||
lbry.stop();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.title = "LBRY is Closed";
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<main className="page">
|
||||
<main className="main--single-column">
|
||||
<h3>LBRY is Closed</h3>
|
||||
<Link href="lbry://lbry" label="Click here to start LBRY" />
|
||||
</main>
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import lbry from '../lbry.js';
|
||||
import {Link} from '../component/link.js';
|
||||
import Modal from '../component/modal.js';
|
||||
import {SubHeader} from '../component/header.js';
|
||||
import {FormField, FormRow} from '../component/form.js';
|
||||
import {Address, BusyMessage, CreditAmount} from '../component/common.js';
|
||||
|
||||
|
@ -263,6 +264,16 @@ var TransactionList = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
export let WalletNav = React.createClass({
|
||||
render: function() {
|
||||
return <SubHeader modifier="constrained" viewingPage={this.props.viewingPage} links={{
|
||||
'?wallet': 'Overview',
|
||||
'?send': 'Send',
|
||||
'?receive': 'Receive',
|
||||
'?rewards': 'Rewards'
|
||||
}} />;
|
||||
}
|
||||
});
|
||||
|
||||
var WalletPage = React.createClass({
|
||||
_balanceSubscribeId: null,
|
||||
|
@ -270,9 +281,6 @@ var WalletPage = React.createClass({
|
|||
propTypes: {
|
||||
viewingPage: React.PropTypes.string,
|
||||
},
|
||||
componentDidMount: function() {
|
||||
document.title = "My Wallet";
|
||||
},
|
||||
/*
|
||||
Below should be refactored so that balance is shared all of wallet page. Or even broader?
|
||||
What is the proper React pattern for sharing a global state like balance?
|
||||
|
@ -296,7 +304,8 @@ var WalletPage = React.createClass({
|
|||
},
|
||||
render: function() {
|
||||
return (
|
||||
<main className="page">
|
||||
<main className="main--single-column">
|
||||
<WalletNav viewingPage={this.props.viewingPage} />
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<h3>Balance</h3>
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
* Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value
|
||||
* is not set yet.
|
||||
*/
|
||||
export function getLocal(key) {
|
||||
export function getLocal(key, fallback=undefined) {
|
||||
const itemRaw = localStorage.getItem(key);
|
||||
return itemRaw === null ? undefined : JSON.parse(itemRaw);
|
||||
return itemRaw === null ? fallback : JSON.parse(itemRaw);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
"babel-cli": "^6.11.4",
|
||||
"babel-preset-es2015": "^6.13.2",
|
||||
"babel-preset-react": "^6.11.1",
|
||||
"clamp-js-main": "^0.11.1",
|
||||
"mediaelement": "^2.23.4",
|
||||
"node-sass": "^3.8.0",
|
||||
"rc-progress": "^2.0.6",
|
||||
|
|
|
@ -1,220 +0,0 @@
|
|||
@import "global";
|
||||
|
||||
html
|
||||
{
|
||||
height: 100%;
|
||||
font-size: $font-size;
|
||||
}
|
||||
body
|
||||
{
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
line-height: $font-line-height;
|
||||
}
|
||||
|
||||
$drawer-width: 220px;
|
||||
|
||||
#drawer
|
||||
{
|
||||
width: $drawer-width;
|
||||
position: fixed;
|
||||
min-height: 100vh;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: $color-bg;
|
||||
z-index: 3;
|
||||
.drawer-item
|
||||
{
|
||||
display: block;
|
||||
padding: $spacing-vertical / 2;
|
||||
font-size: 1.2em;
|
||||
height: $spacing-vertical * 1.5;
|
||||
.icon
|
||||
{
|
||||
margin-right: 6px;
|
||||
}
|
||||
.link-label
|
||||
{
|
||||
line-height: $spacing-vertical * 1.5;
|
||||
}
|
||||
.badge
|
||||
{
|
||||
float: right;
|
||||
margin-top: $spacing-vertical * 0.25 - 2;
|
||||
background: $color-money;
|
||||
}
|
||||
}
|
||||
.drawer-item-selected
|
||||
{
|
||||
background: $color-canvas;
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
.badge
|
||||
{
|
||||
background: $color-money;
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.credit-amount--indicator
|
||||
{
|
||||
font-weight: bold;
|
||||
color: $color-money;
|
||||
}
|
||||
#drawer-handle
|
||||
{
|
||||
padding: $spacing-vertical / 2;
|
||||
max-height: $height-header - $spacing-vertical;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#window
|
||||
{
|
||||
position: relative; /*window has it's own z-index inside of it*/
|
||||
z-index: 1;
|
||||
}
|
||||
#window.drawer-closed
|
||||
{
|
||||
#drawer { display: none }
|
||||
}
|
||||
#window.drawer-open
|
||||
{
|
||||
#main-content { margin-left: $drawer-width; }
|
||||
.open-drawer-link { display: none }
|
||||
#header { padding-left: $drawer-width + $spacing-vertical / 2; }
|
||||
}
|
||||
|
||||
#header
|
||||
{
|
||||
background: $color-primary;
|
||||
color: white;
|
||||
&.header-no-subnav {
|
||||
height: $height-header;
|
||||
}
|
||||
&.header-with-subnav {
|
||||
height: $height-header * 2;
|
||||
}
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
h1 { font-size: 1.8em; line-height: $height-header - $spacing-vertical; display: inline-block; float: left; }
|
||||
&.header-scrolled
|
||||
{
|
||||
box-shadow: $default-box-shadow;
|
||||
}
|
||||
}
|
||||
.header-top-bar
|
||||
{
|
||||
padding: $spacing-vertical / 2;
|
||||
}
|
||||
.header-search
|
||||
{
|
||||
margin-left: 60px;
|
||||
$padding-adjust: 36px;
|
||||
text-align: center;
|
||||
.icon {
|
||||
position: absolute;
|
||||
top: $spacing-vertical * 1.5 / 2 + 2px; //hacked
|
||||
margin-left: -$padding-adjust + 14px; //hacked
|
||||
}
|
||||
input[type="search"] {
|
||||
position: relative;
|
||||
left: -$padding-adjust;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
width: 400px;
|
||||
height: $spacing-vertical * 1.5;
|
||||
line-height: $spacing-vertical * 1.5;
|
||||
padding-left: $padding-adjust + 3;
|
||||
padding-right: 3px;
|
||||
@include border-radius(2px);
|
||||
@include placeholder-color(#e8e8e8);
|
||||
&:focus {
|
||||
box-shadow: $focus-box-shadow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nav.sub-header
|
||||
{
|
||||
background: $color-primary;
|
||||
text-transform: uppercase;
|
||||
padding: $spacing-vertical / 2;
|
||||
> a
|
||||
{
|
||||
$sub-header-selected-underline-height: 2px;
|
||||
display: inline-block;
|
||||
margin: 0 15px;
|
||||
padding: 0 5px;
|
||||
line-height: $height-header - $spacing-vertical - $sub-header-selected-underline-height;
|
||||
color: #e8e8e8;
|
||||
&:first-child
|
||||
{
|
||||
margin-left: 0;
|
||||
}
|
||||
&:last-child
|
||||
{
|
||||
margin-right: 0;
|
||||
}
|
||||
&.sub-header-selected
|
||||
{
|
||||
border-bottom: $sub-header-selected-underline-height solid #fff;
|
||||
color: #fff;
|
||||
}
|
||||
&:hover
|
||||
{
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#main-content
|
||||
{
|
||||
background: $color-canvas;
|
||||
&.no-sub-nav
|
||||
{
|
||||
min-height: calc(100vh - 60px); //should be -$height-header, but I'm dumb I guess? It wouldn't work
|
||||
main { margin-top: $height-header; }
|
||||
}
|
||||
&.with-sub-nav
|
||||
{
|
||||
min-height: calc(100vh - 120px); //should be -$height-header, but I'm dumb I guess? It wouldn't work
|
||||
main { margin-top: $height-header * 2; }
|
||||
}
|
||||
main
|
||||
{
|
||||
padding: $spacing-vertical;
|
||||
&.constrained-page
|
||||
{
|
||||
max-width: $width-page-constrained;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$header-icon-size: 1.5em;
|
||||
|
||||
.open-drawer-link, .close-drawer-link
|
||||
{
|
||||
display: inline-block;
|
||||
font-size: $header-icon-size;
|
||||
padding: 2px 6px 0 6px;
|
||||
float: left;
|
||||
}
|
||||
.close-lbry-link
|
||||
{
|
||||
font-size: $header-icon-size;
|
||||
float: right;
|
||||
padding: 0 6px 0 18px;
|
||||
}
|
||||
|
||||
.full-screen
|
||||
{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
|
@ -34,8 +34,8 @@ $height-header: $spacing-vertical * 2.5;
|
|||
$height-button: $spacing-vertical * 1.5;
|
||||
$height-video-embedded: $width-page-constrained * 9 / 16;
|
||||
|
||||
$default-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
|
||||
$focus-box-shadow: 2px 4px 4px 0 rgba(0,0,0,.14),2px 5px 3px -2px rgba(0,0,0,.2),2px 3px 7px 0 rgba(0,0,0,.12);
|
||||
$box-shadow-layer: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
|
||||
$box-shadow-focus: 2px 4px 4px 0 rgba(0,0,0,.14),2px 5px 3px -2px rgba(0,0,0,.2),2px 3px 7px 0 rgba(0,0,0,.12);
|
||||
|
||||
$transition-standard: .225s ease;
|
||||
|
||||
|
@ -161,3 +161,34 @@ $blur-intensity: 8px;
|
|||
height:1px;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
@mixin text-link($color: $color-primary, $hover-opacity: 0.70) {
|
||||
.icon
|
||||
{
|
||||
&:first-child {
|
||||
padding-right: 5px;
|
||||
}
|
||||
&:last-child:not(:only-child) {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.no-underline) {
|
||||
text-decoration: underline;
|
||||
.icon {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
&:hover
|
||||
{
|
||||
opacity: $hover-opacity;
|
||||
transition: opacity $transition-standard;
|
||||
text-decoration: underline;
|
||||
.icon {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
color: $color;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
@import "global";
|
||||
|
||||
$gutter_fluid: 4;
|
||||
|
||||
[class*="span"] {
|
||||
min-height: 1px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.span12 { width: 100%; }
|
||||
.span11 { width: 91.666%; }
|
||||
.span10 { width: 83.333%; }
|
||||
.span9 { width: 75%; }
|
||||
.span8 { width: 66.666%; }
|
||||
.span7 { width: 58.333%; }
|
||||
.span6 { width: 50%; }
|
||||
.span5 { width: 41.666%; }
|
||||
.span4 { width: 33.333%; }
|
||||
.span3 { width: 25%; }
|
||||
.span2 { width: 16.666%; }
|
||||
.span1 { width: 8.333%; }
|
||||
|
||||
.row-fluid {
|
||||
width: 100%;
|
||||
> [class*="span"] {
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin-left: 1% * $gutter_fluid;
|
||||
&:first-child
|
||||
{
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
$column_width: (100% - $gutter_fluid * 11) / 12;
|
||||
|
||||
> .span12 { width: $column_width * 12 + $gutter_fluid * 11; }
|
||||
> .span11 { width: $column_width * 11 + $gutter_fluid * 10; }
|
||||
> .span10 { width: $column_width * 10 + $gutter_fluid * 9; }
|
||||
> .span9 { width: $column_width * 9 + $gutter_fluid * 8; }
|
||||
> .span8 { width: $column_width * 8 + $gutter_fluid * 7; }
|
||||
> .span7 { width: $column_width * 7 + $gutter_fluid * 6; }
|
||||
> .span6 { width: $column_width * 6 + $gutter_fluid * 5; }
|
||||
> .span5 { width: $column_width * 5 + $gutter_fluid * 4; }
|
||||
> .span4 { width: $column_width * 4 + $gutter_fluid * 3; }
|
||||
> .span3 { width: $column_width * 3 + $gutter_fluid * 2; }
|
||||
> .span2 { width: $column_width * 2 + $gutter_fluid * 1; }
|
||||
> .span1 { width: $column_width; }
|
||||
}
|
||||
|
||||
.tile-fluid {
|
||||
width: 100%;
|
||||
> [class*="span"] {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.column-fluid {
|
||||
@include display-flex();
|
||||
flex-wrap: wrap;
|
||||
> [class*="span"] {
|
||||
@include display-flex();
|
||||
@include flex(1 0 auto);
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.row-fluid, .tile-fluid {
|
||||
@include clearfix();
|
||||
}
|
||||
|
||||
@media (max-width: $mobile-width-threshold) {
|
||||
.row-fluid, .tile-fluid, .column-fluid {
|
||||
width: 100%;
|
||||
}
|
||||
.pull-left, .pull-right
|
||||
{
|
||||
float: none;
|
||||
}
|
||||
[class*="span"] {
|
||||
float: none !important;
|
||||
width: 100% !important;
|
||||
margin-left: 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
}
|
|
@ -1,35 +1,51 @@
|
|||
@import "global";
|
||||
|
||||
@mixin text-link($color: $color-primary, $hover-opacity: 0.70) {
|
||||
|
||||
.icon
|
||||
html
|
||||
{
|
||||
&:first-child {
|
||||
padding-right: 5px;
|
||||
height: 100%;
|
||||
font-size: $font-size;
|
||||
}
|
||||
&:last-child:not(:only-child) {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.no-underline) {
|
||||
text-decoration: underline;
|
||||
.icon {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
&:hover
|
||||
body
|
||||
{
|
||||
opacity: $hover-opacity;
|
||||
transition: opacity $transition-standard;
|
||||
text-decoration: underline;
|
||||
.icon {
|
||||
text-decoration: none;
|
||||
}
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
line-height: $font-line-height;
|
||||
}
|
||||
|
||||
color: $color;
|
||||
cursor: pointer;
|
||||
#window
|
||||
{
|
||||
min-height: 100vh;
|
||||
background: $color-canvas;
|
||||
}
|
||||
|
||||
.badge
|
||||
{
|
||||
background: $color-money;
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.credit-amount--indicator
|
||||
{
|
||||
font-weight: bold;
|
||||
color: $color-money;
|
||||
}
|
||||
|
||||
#main-content
|
||||
{
|
||||
padding: $spacing-vertical;
|
||||
margin-top: $height-header;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
main {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
main.main--single-column
|
||||
{
|
||||
width: $width-page-constrained;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-fixed-width {
|
||||
|
@ -80,7 +96,10 @@ p
|
|||
}
|
||||
|
||||
.truncated-text {
|
||||
display: inline-block;
|
||||
//display: inline-block;
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.busy-indicator
|
||||
|
@ -126,10 +145,11 @@ p
|
|||
|
||||
.sort-section {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: $spacing-vertical * 2/3;
|
||||
|
||||
|
||||
text-align: right;
|
||||
line-height: 1;
|
||||
font-size: 0.85em;
|
||||
color: $color-help;
|
||||
}
|
||||
|
|
|
@ -25,12 +25,6 @@
|
|||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.icon-mega
|
||||
{
|
||||
font-size: 200px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
|
||||
readers do not read off random characters that represent icons */
|
||||
.icon-glass:before {
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
@import "_reset";
|
||||
@import "_grid";
|
||||
@import "_icons";
|
||||
@import "_mediaelement";
|
||||
@import "_canvas";
|
||||
@import "_gui";
|
||||
@import "component/_table";
|
||||
@import "component/_button.scss";
|
||||
|
@ -10,6 +8,7 @@
|
|||
@import "component/_file-actions.scss";
|
||||
@import "component/_file-tile.scss";
|
||||
@import "component/_form-field.scss";
|
||||
@import "component/_header.scss";
|
||||
@import "component/_menu.scss";
|
||||
@import "component/_tooltip.scss";
|
||||
@import "component/_load-screen.scss";
|
||||
|
|
|
@ -34,6 +34,11 @@ $button-focus-shift: 12%;
|
|||
{
|
||||
padding-left: 5px;
|
||||
}
|
||||
.icon:only-child
|
||||
{
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
.button-block
|
||||
{
|
||||
|
@ -49,17 +54,17 @@ $button-focus-shift: 12%;
|
|||
$color-button-text: white;
|
||||
color: darken($color-button-text, $button-focus-shift * 0.5);
|
||||
background-color: $color-primary;
|
||||
box-shadow: $default-box-shadow;
|
||||
box-shadow: $box-shadow-layer;
|
||||
&:focus {
|
||||
color: $color-button-text;
|
||||
//box-shadow: $focus-box-shadow;
|
||||
//box-shadow: $box-shadow-focus;
|
||||
background-color: mix(black, $color-primary, $button-focus-shift)
|
||||
}
|
||||
}
|
||||
.button-alt
|
||||
{
|
||||
background-color: $color-bg-alt;
|
||||
box-shadow: $default-box-shadow;
|
||||
box-shadow: $box-shadow-layer;
|
||||
}
|
||||
|
||||
.button-text
|
||||
|
@ -76,3 +81,7 @@ $button-focus-shift: 12%;
|
|||
@include text-link(#aaa);
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.button--flat
|
||||
{
|
||||
box-shadow: none !important;
|
||||
}
|
|
@ -7,7 +7,7 @@ $padding-card-horizontal: $spacing-vertical * 2/3;
|
|||
margin-right: auto;
|
||||
max-width: $width-page-constrained;
|
||||
background: $color-bg;
|
||||
box-shadow: $default-box-shadow;
|
||||
box-shadow: $box-shadow-layer;
|
||||
border-radius: 2px;
|
||||
margin-bottom: $spacing-vertical * 2/3;
|
||||
overflow: auto;
|
||||
|
@ -86,7 +86,7 @@ $card-link-scaling: 1.1;
|
|||
.card--link:hover {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-shadow: $focus-box-shadow;
|
||||
box-shadow: $box-shadow-focus;
|
||||
transform: scale($card-link-scaling);
|
||||
transform-origin: 50% 50%;
|
||||
overflow-x: visible;
|
||||
|
@ -139,8 +139,12 @@ $height-card-small: $spacing-vertical * 15;
|
|||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
/*hacky way to give space for hover */
|
||||
padding-left: 20px;
|
||||
margin-left: -20px; /*hacky way to give space for hover */
|
||||
margin-left: -20px;
|
||||
padding-right: 20px;
|
||||
margin-right: -20px;
|
||||
}
|
||||
.card-row__header {
|
||||
margin-bottom: $spacing-vertical / 3;
|
||||
|
|
|
@ -1,37 +1,26 @@
|
|||
@import "../global";
|
||||
|
||||
$height-file-tile: $spacing-vertical * 8;
|
||||
$height-file-tile: $spacing-vertical * 6;
|
||||
.file-tile__row {
|
||||
overflow: hidden;
|
||||
height: $height-file-tile;
|
||||
.credit-amount {
|
||||
float: right;
|
||||
}
|
||||
//Hack! Remove below!
|
||||
.card__title-primary {
|
||||
margin-top: $spacing-vertical * 2/3;
|
||||
}
|
||||
}
|
||||
|
||||
.file-tile__thumbnail {
|
||||
max-width: 100%;
|
||||
max-height: $height-file-tile;
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.file-tile__thumbnail-container
|
||||
{
|
||||
//also a hack
|
||||
.card__media {
|
||||
height: $height-file-tile;
|
||||
@include absolute-center();
|
||||
max-width: $height-file-tile;
|
||||
width: $height-file-tile;
|
||||
margin-right: $spacing-vertical / 2;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.file-tile__title {
|
||||
font-weight: bold;
|
||||
//basically everything here is a hack now
|
||||
.file-tile__content {
|
||||
padding-top: $spacing-vertical * 1/3;
|
||||
margin-left: $height-file-tile + $spacing-vertical / 2;
|
||||
}
|
||||
.card__title-primary {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.file-tile__description {
|
||||
color: #444;
|
||||
margin-top: 12px;
|
||||
font-size: 0.9em;
|
||||
}
|
94
ui/scss/component/_header.scss
Normal file
94
ui/scss/component/_header.scss
Normal file
|
@ -0,0 +1,94 @@
|
|||
@import "../global";
|
||||
|
||||
$color-header: #666;
|
||||
$color-header-active: darken($color-header, 20%);
|
||||
|
||||
#header
|
||||
{
|
||||
color: $color-header;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
box-shadow: $box-shadow-layer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
padding: $spacing-vertical / 2;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header__item {
|
||||
flex: 0 0 content;
|
||||
padding-left: $spacing-vertical / 4;
|
||||
padding-right: $spacing-vertical / 4;
|
||||
}
|
||||
.header__item--wunderbar {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.wunderbar
|
||||
{
|
||||
position: relative;
|
||||
.icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: $spacing-vertical / 2 - 4px; //hacked
|
||||
}
|
||||
}
|
||||
|
||||
.wunderbar--active .icon-search { color: $color-primary; }
|
||||
|
||||
.wunderbar__input {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
width: 100%;
|
||||
color: $color-header;
|
||||
height: $spacing-vertical * 1.5;
|
||||
line-height: $spacing-vertical * 1.5;
|
||||
padding-left: 38px;
|
||||
padding-right: 5px;
|
||||
border: 1px solid $color-text-dark;
|
||||
@include border-radius(2px);
|
||||
border: 1px solid #ccc;
|
||||
&:focus {
|
||||
color: $color-header-active;
|
||||
box-shadow: $box-shadow-focus;
|
||||
border-color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
nav.sub-header
|
||||
{
|
||||
text-transform: uppercase;
|
||||
padding: 0 0 $spacing-vertical;
|
||||
&.sub-header--constrained {
|
||||
max-width: $width-page-constrained;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
> a
|
||||
{
|
||||
$sub-header-selected-underline-height: 2px;
|
||||
display: inline-block;
|
||||
margin: 0 15px;
|
||||
padding: 0 5px;
|
||||
line-height: $height-header - $spacing-vertical - $sub-header-selected-underline-height;
|
||||
color: $color-header;
|
||||
&:first-child
|
||||
{
|
||||
margin-left: 0;
|
||||
}
|
||||
&:last-child
|
||||
{
|
||||
margin-right: 0;
|
||||
}
|
||||
&.sub-header-selected
|
||||
{
|
||||
border-bottom: $sub-header-selected-underline-height solid $color-header-active;
|
||||
color: $color-header-active;
|
||||
}
|
||||
&:hover
|
||||
{
|
||||
color: $color-header-active;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ $border-radius-menu: 2px;
|
|||
position: absolute;
|
||||
white-space: nowrap;
|
||||
background-color: white;
|
||||
box-shadow: $default-box-shadow;
|
||||
box-shadow: $box-shadow-layer;
|
||||
border-radius: $border-radius-menu;
|
||||
padding-top: ($spacing-vertical / 5) 0px;
|
||||
z-index: 1;
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
overflow: auto;
|
||||
border-radius: 4px;
|
||||
padding: $spacing-vertical;
|
||||
box-shadow: $default-box-shadow;
|
||||
box-shadow: $box-shadow-layer;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
z-index: 1;
|
||||
left: 50%;
|
||||
margin-left: $tooltip-body-width * -1 / 2;
|
||||
white-space: normal;
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: $spacing-vertical / 2;
|
||||
|
@ -24,7 +25,7 @@
|
|||
background-color: $color-bg;
|
||||
font-size: $font-size * 7/8;
|
||||
line-height: $font-line-height;
|
||||
box-shadow: $default-box-shadow;
|
||||
box-shadow: $box-shadow-layer;
|
||||
}
|
||||
|
||||
.tooltip--header .tooltip__link {
|
||||
|
|
Loading…
Reference in a new issue