Home landing page (#4178)

* Home entry point (basics)

* WIP store for web

* Add DappUrlInput component

* Updated tests

* WIP store update

* Adjust styling

* Add home tab

* Collapse first/last without extra divs

* Navigation actually navigates

* styling

* Encoding of ethlink.io URLs

* encodedUrl setup

* base58 encoded URLs

* Added decoding, updated tests to Parity-compliant

* Base32 (synced with Rust implementation via tests)

* Split URL into 63 character chunks

* Fix store test

* Cleanups

* s/ethlink/dapplink/

* Display app navigation & histroy

* Start on /accounts (for now, until expanded fully)

* Update tests

* ethlink.io -> web3.site

* Basic list layout

* Store history on navigation

* Show Accounts & Dapps

* Add skeleton for DappIcon (WIP)

* DappIcon WIP

* DappIcon in place

* Split into maneable sub-components

* WIP

* Tests for views/Home

* Swap default entry-point to /home

* Expose registry.get via lookupMeta

* Add getEntry interface, fix instance retrieval (with tests)

* Add news display component

* Add tests for added contracts/registry methods

* Fix GHH test refactoring

* render news via SectionList

* News items store directly

* Images

* News & Urls has new layout

* Convert remainder

* First run-through of MVP for SectionList

* Update tests

* Deploycontract should not override global p styles

* Allow styles overrides for head & body

* Adjust layout styling

* revert Container>flex

* Adjust sizes of history items

* Cleanups

* HistoryStore for tracking relevant routes

* Default route is still /accounts

* Fix tests

* Update 2015-2017

* Add lookupMeta & tests

* Add getEntry & tests

* Split Dapp icon into ui/DappIcon

* Update copyright dates

* Encoding for *.web3.site urls

* Dapp history retrieval

* Grow to only 40% on hover

* Update description

* Add DappUrlInput component

* Update Web views with store

* Update spec description

* Update spec description

* edited url does not allow in-place store edits

* Use /web/<hash> urls for iframe

* Removed (now) unused _list.css

* Mistamtched merge fixed

* Tab split (WIP)

* Split Tab component

* Update tests after merge

* typo

* Remove background !important

* Set item width to parent

* Set width, remove overflow-x: hidden

* Align hover overlays

* Container defaults to some opacity

* Display history from listRecentDapps

* Override styles for a tags

* Open URLs in new window when extension is available

* Fix tests after update

* AccountCard width 100%

* Re-add opening correct url in tab

* Cleanup link rendering

* Remove hardcoded news URL

* pre-merge

* Extra padding at Home bottom (Pr grumble)

* Match js-vaults stretch

* s/Web Apps via URL/Web Apps/ (PR grumble)

* Store recent wallets (PR grumble)

* Simplify inline style matching (PR comment)

* Add store for new retrieval

* Add missing observer

* Auto-link based on account type

* Fix UI overlaps

* Extra spacing

* Only show account when accountInfo is available

* Align timestamp line-heights

* Fix tests

* Update tests

* Really fix failing test (check for Connect(Account))
This commit is contained in:
Jaco Greeff 2017-02-14 08:29:32 +01:00 committed by GitHub
parent 78917d728d
commit 63d2cfcbfc
59 changed files with 2014 additions and 344 deletions

View File

@ -183,6 +183,7 @@
"react-element-to-jsx-string": "6.0.0",
"react-event-listener": "0.4.1",
"react-intl": "2.1.5",
"react-markdown": "2.4.4",
"react-portal": "3.0.0",
"react-redux": "4.4.6",
"react-router": "3.0.0",

View File

@ -181,6 +181,16 @@ export function outReceipt (receipt) {
return receipt;
}
export function outRecentDapps (recentDapps) {
if (recentDapps) {
Object.keys(recentDapps).forEach((url) => {
recentDapps[url] = outDate(recentDapps[url]);
});
}
return recentDapps;
}
export function outSignerRequest (request) {
if (request) {
Object.keys(request).forEach((key) => {

View File

@ -16,7 +16,7 @@
import BigNumber from 'bignumber.js';
import { outBlock, outAccountInfo, outAddress, outChainStatus, outDate, outHistogram, outNumber, outPeer, outPeers, outReceipt, outSyncing, outTransaction, outTrace, outVaultMeta } from './output';
import { outBlock, outAccountInfo, outAddress, outChainStatus, outDate, outHistogram, outNumber, outPeer, outPeers, outReceipt, outRecentDapps, outSyncing, outTransaction, outTrace, outVaultMeta } from './output';
import { isAddress, isBigNumber, isInstanceOf } from '../../../test/types';
describe('api/format/output', () => {
@ -337,6 +337,14 @@ describe('api/format/output', () => {
});
});
describe('outRecentDapps', () => {
it('formats the URLs with timestamps', () => {
expect(outRecentDapps({ testing: 0x57513668 })).to.deep.equal({
testing: new Date('2016-06-03T07:48:56.000Z')
});
});
});
describe('outSyncing', () => {
['currentBlock', 'highestBlock', 'startingBlock', 'warpChunksAmount', 'warpChunksProcessed'].forEach((input) => {
it(`formats ${input} numbers as a number`, () => {

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { inAddress, inAddresses, inData, inHex, inNumber16, inOptions, inBlockNumber } from '../../format/input';
import { outAccountInfo, outAddress, outAddresses, outChainStatus, outHistogram, outNumber, outPeers, outTransaction, outVaultMeta } from '../../format/output';
import { outAccountInfo, outAddress, outAddresses, outChainStatus, outHistogram, outNumber, outPeers, outRecentDapps, outTransaction, outVaultMeta } from '../../format/output';
export default class Parity {
constructor (transport) {
@ -222,7 +222,8 @@ export default class Parity {
listRecentDapps () {
return this._transport
.execute('parity_listRecentDapps');
.execute('parity_listRecentDapps')
.then(outRecentDapps);
}
listStorageKeys (address, count, hash = null, blockNumber = 'latest') {

View File

@ -48,6 +48,10 @@ export default {
label: 'Contracts'
},
home: {
label: 'Home'
},
status: {
label: 'Status'
},

View File

@ -91,7 +91,7 @@ export default class ParametersStep extends Component {
});
return (
<div>
<div className={ styles.parameters }>
<p>
<FormattedMessage
id='deployContract.parameters.choose'

View File

@ -32,6 +32,8 @@
padding-left: 3em;
}
p {
color: rgba(255, 255, 255, 0.498039);
.parameters {
p {
color: rgba(255, 255, 255, 0.498039);
}
}

View File

@ -16,7 +16,7 @@
import {
Accounts, Account, Addresses, Address, Application,
Contract, Contracts, Dapp, Dapps, HistoryStore,
Contract, Contracts, Dapp, Dapps, HistoryStore, Home,
Settings, SettingsBackground, SettingsParity, SettingsProxy,
SettingsViews, Signer, Status,
Wallet, Web, WriteContract
@ -54,10 +54,16 @@ const accountsRoutes = [
path: ':address',
component: Account,
onEnter: ({ params }) => {
accountsHistory.add(params.address);
accountsHistory.add(params.address, 'account');
}
},
{ path: '/wallet/:address', component: Wallet }
{
path: '/wallet/:address',
component: Wallet,
onEnter: ({ params }) => {
accountsHistory.add(params.address, 'wallet');
}
}
];
const addressesRoutes = [
@ -86,8 +92,8 @@ const routes = [
{ path: '/address/:address', onEnter: handleDeprecatedRoute },
{ path: '/contract/:address', onEnter: handleDeprecatedRoute },
{ path: '/', onEnter: redirectTo('/accounts') },
{ path: '/auth', onEnter: redirectTo('/accounts') },
{ path: '/', onEnter: redirectTo('/home') },
{ path: '/auth', onEnter: redirectTo('/home') },
{ path: '/settings', onEnter: redirectTo('/settings/views') }
];
@ -127,6 +133,7 @@ const childRoutes = [
}
},
{ path: 'apps', component: Dapps },
{ path: 'home', component: Home },
{ path: 'web', component: Web },
{ path: 'web/:url', component: Web },
{ path: 'signer', component: Signer }

View File

@ -14,11 +14,37 @@
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
$background: rgba(18, 18, 18, 0.85);
$backgroundOverlay: rgba(18, 18, 18, 1);
.container {
background: $background;
flex: 1;
padding: 0em;
background: rgba(0, 0, 0, 0.8);
height: 100%;
padding: 0em;
transition: all 0.75s cubic-bezier(0.23, 1, 0.32, 1);
width: 100%;
.hoverOverlay {
background: $backgroundOverlay;
display: none;
left: 0;
margin-top: -1.5em;
padding: 0 1.5em 1.5em 1.5em;
position: absolute;
right: 0;
top: 100%;
z-index: 100;
}
&:hover {
background: $backgroundOverlay;
.hoverOverlay {
display: block;
}
}
}
.compact,

View File

@ -28,6 +28,7 @@ export default class Container extends Component {
children: PropTypes.node,
className: PropTypes.string,
compact: PropTypes.bool,
hover: PropTypes.node,
light: PropTypes.bool,
onClick: PropTypes.func,
style: PropTypes.object,
@ -68,10 +69,25 @@ export default class Container extends Component {
{ this.renderTitle() }
{ children }
</Card>
{ this.renderHover() }
</div>
);
}
renderHover () {
const { hover } = this.props;
if (!hover) {
return null;
}
return (
<Card className={ styles.hoverOverlay }>
{ hover }
</Card>
);
}
renderTitle () {
const { title } = this.props;

View File

@ -37,10 +37,17 @@ describe('ui/Container', () => {
});
describe('sections', () => {
it('renders the Card', () => {
it('renders the default Card', () => {
expect(render().find('Card')).to.have.length(1);
});
it('renders Hover Card when available', () => {
const cards = render({ hover: <div>testingHover</div> }).find('Card');
expect(cards).to.have.length(2);
expect(cards.get(1).props.children.props.children).to.equal('testingHover');
});
it('renders the Title', () => {
const title = render({ title: 'title' }).find('Title');

View File

@ -14,66 +14,33 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import AddIcon from 'material-ui/svg-icons/content/add';
import AttachFileIcon from 'material-ui/svg-icons/editor/attach-file';
import CancelIcon from 'material-ui/svg-icons/content/clear';
import CheckIcon from 'material-ui/svg-icons/navigation/check';
import CloseIcon from 'material-ui/svg-icons/navigation/close';
import CompareIcon from 'material-ui/svg-icons/action/compare-arrows';
import ComputerIcon from 'material-ui/svg-icons/hardware/desktop-mac';
import ContractIcon from 'material-ui/svg-icons/action/code';
import CopyIcon from 'material-ui/svg-icons/content/content-copy';
import DashboardIcon from 'material-ui/svg-icons/action/dashboard';
import DeleteIcon from 'material-ui/svg-icons/action/delete';
import DoneIcon from 'material-ui/svg-icons/action/done-all';
import EditIcon from 'material-ui/svg-icons/content/create';
import FingerprintIcon from 'material-ui/svg-icons/action/fingerprint';
import LinkIcon from 'material-ui/svg-icons/content/link';
import LockedIcon from 'material-ui/svg-icons/action/lock';
import MoveIcon from 'material-ui/svg-icons/action/open-with';
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
import PrintIcon from 'material-ui/svg-icons/action/print';
import RefreshIcon from 'material-ui/svg-icons/action/autorenew';
import SaveIcon from 'material-ui/svg-icons/content/save';
import SendIcon from 'material-ui/svg-icons/content/send';
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
import StarCircleIcon from 'material-ui/svg-icons/action/stars';
import StarIcon from 'material-ui/svg-icons/toggle/star';
import StarOutlineIcon from 'material-ui/svg-icons/toggle/star-border';
import VerifyIcon from 'material-ui/svg-icons/action/verified-user';
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';
export {
AddIcon,
AttachFileIcon,
CancelIcon,
CheckIcon,
CloseIcon,
CompareIcon,
ComputerIcon,
ContractIcon,
CopyIcon,
DashboardIcon,
DeleteIcon,
DoneIcon,
EditIcon,
FingerprintIcon,
LinkIcon,
LockedIcon,
MoveIcon,
NextIcon,
PrevIcon,
PrintIcon,
RefreshIcon,
SaveIcon,
SendIcon,
SnoozeIcon,
StarIcon,
StarCircleIcon,
StarOutlineIcon,
VerifyIcon,
VisibleIcon,
VpnIcon
};
export AddIcon from 'material-ui/svg-icons/content/add';
export AttachFileIcon from 'material-ui/svg-icons/editor/attach-file';
export CancelIcon from 'material-ui/svg-icons/content/clear';
export CheckIcon from 'material-ui/svg-icons/navigation/check';
export CloseIcon from 'material-ui/svg-icons/navigation/close';
export CompareIcon from 'material-ui/svg-icons/action/compare-arrows';
export ComputerIcon from 'material-ui/svg-icons/hardware/desktop-mac';
export ContractIcon from 'material-ui/svg-icons/action/code';
export CopyIcon from 'material-ui/svg-icons/content/content-copy';
export DashboardIcon from 'material-ui/svg-icons/action/dashboard';
export DeleteIcon from 'material-ui/svg-icons/action/delete';
export DoneIcon from 'material-ui/svg-icons/action/done-all';
export EditIcon from 'material-ui/svg-icons/content/create';
export FingerprintIcon from 'material-ui/svg-icons/action/fingerprint';
export LinkIcon from 'material-ui/svg-icons/content/link';
export LockedIcon from 'material-ui/svg-icons/action/lock';
export MoveIcon from 'material-ui/svg-icons/action/open-with';
export NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
export PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
export PrintIcon from 'material-ui/svg-icons/action/print';
export RefreshIcon from 'material-ui/svg-icons/action/autorenew';
export SaveIcon from 'material-ui/svg-icons/content/save';
export SendIcon from 'material-ui/svg-icons/content/send';
export SnoozeIcon from 'material-ui/svg-icons/av/snooze';
export StarCircleIcon from 'material-ui/svg-icons/action/stars';
export StarIcon from 'material-ui/svg-icons/toggle/star';
export StarOutlineIcon from 'material-ui/svg-icons/toggle/star-border';
export VerifyIcon from 'material-ui/svg-icons/action/verified-user';
export VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
export VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';

View File

@ -16,8 +16,8 @@
*/
.section {
overflow-x: hidden;
position: relative;
width: 100%;
.overlay {
background: rgba(0, 0, 0, 0.85);
@ -33,7 +33,6 @@
.row {
display: flex;
justify-content: center;
overflow-x: hidden;
/* TODO: As per JS comments, the flex-base could be adjusted in the future to allow for */
/* case where <> 3 columns are required should the need arrise from a UI pov. */
@ -42,38 +41,28 @@
cursor: pointer;
display: flex;
flex: 0 1 33.33%;
opacity: 0.75;
overflow-x: hidden;
max-width: 33.33%;
opacity: 0.85;
padding: 0.25em;
transition: all 0.75s cubic-bezier(0.23, 1, 0.32, 1);
/* TODO: The hover and no-hover states can be improved to not "just appear" */
&:not(:hover) {
& [data-hover="hide"] {
}
& [data-hover="show"] {
display: none;
}
}
&:hover {
opacity: 1;
z-index: 100;
& [data-hover="hide"] {
display: none;
}
& [data-hover="show"] {
}
}
}
&.stretch-on:hover {
flex: 0 0 50%;
}
&:hover {
.item {
&.stretchOn {
flex: 0 1 29%;
max-width: 29%;
&.stretch-off:hover {
&:hover {
flex: 0 0 42%;
max-width: 42%;
}
}
}
}
}

View File

@ -74,29 +74,34 @@ export default class SectionList extends Component {
className={ styles.row }
key={ `row_${index}` }
>
{ row.map(this.renderItem) }
{
row
.map(this.renderItem)
.filter((item) => item)
}
</div>
);
}
renderItem = (item, index) => {
const { noStretch, renderItem } = this.props;
const itemRendered = renderItem(item, index);
if (!itemRendered) {
return null;
}
// NOTE: Any children that is to be showed or hidden (depending on hover state)
// should have the data-hover="show|hide" attributes. For the current implementation
// this does the trick, however there may be a case for adding a hover attribute
// to an item (mouseEnter/mouseLeave events) and then adjusting the styling with
// :root[hover]/:root:not[hover] for the tragetted elements. Currently it is a
// CSS-only solution to let the browser do all the work via selectors.
return (
<div
className={ [
styles.item,
styles[`stretch-${noStretch ? 'off' : 'on'}`]
noStretch
? styles.stretchOff
: styles.stretchOn
].join(' ') }
key={ `item_${index}` }
>
{ renderItem(item, index) }
{ itemRendered }
</div>
);
}

View File

@ -27,7 +27,7 @@ let instance;
let renderItem;
function render (props = {}) {
renderItem = sinon.stub();
renderItem = sinon.stub().returns('someThing');
component = shallow(
<SectionList
className='testClass'

View File

@ -23,7 +23,7 @@ const lightTheme = getMuiTheme(lightBaseTheme);
const muiTheme = getMuiTheme(darkBaseTheme);
muiTheme.inkBar.backgroundColor = 'transparent';
muiTheme.paper.backgroundColor = 'rgba(0, 0, 0, 0.95)';
muiTheme.paper.backgroundColor = 'rgb(18, 18, 18)';
muiTheme.raisedButton.primaryTextColor = 'white';
muiTheme.snackbar.backgroundColor = 'rgba(255, 30, 30, 0.9)';
muiTheme.snackbar.textColor = 'rgba(255, 255, 255, 0.75)';

View File

@ -14,32 +14,33 @@
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.list {
display: flex;
flex-wrap: wrap;
}
.item {
flex: 0 1 50%;
width: 50%;
position: relative;
padding-bottom: 0.25em;
box-sizing: border-box;
}
.item {
box-sizing: border-box;
flex: 0 1 50%;
padding-bottom: 0.25em;
position: relative;
width: 50%;
.item:nth-child(odd) {
padding-right: 0.125em;
}
&:nth-child(odd) {
padding-right: 0.125em;
}
.item:nth-child(even) {
padding-left: 0.125em;
&:nth-child(even) {
padding-left: 0.125em;
}
}
}
.empty {
width: 100%;
display: block;
}
width: 100%;
.empty div {
color: #aaa;
div {
color: #aaa;
}
}

View File

@ -193,7 +193,11 @@ export default class Summary extends Component {
const viewLink = `/${baseLink}/${address}`;
const content = (
<IdentityName address={ address } name={ name } unknown />
<IdentityName
address={ address }
name={ name }
unknown
/>
);
if (noLink) {

View File

@ -26,7 +26,7 @@ import styles from './extension.css';
@observer
export default class Extension extends Component {
store = new Store();
store = Store.get();
render () {
const { showWarning } = this.store;

View File

@ -29,7 +29,10 @@ const NEXT_DISPLAY = '_parity::extensionWarning::nextDisplay';
// 'https://chrome.google.com/webstore/detail/parity-ethereum-integrati/himekenlppkgeaoeddcliojfddemadig';
const EXTENSION_PAGE = 'https://chrome.google.com/webstore/detail/himekenlppkgeaoeddcliojfddemadig';
let instance;
export default class Store {
@observable hasExtension = false;
@observable isInstalling = false;
@observable nextDisplay = 0;
@observable shouldInstall = false;
@ -43,6 +46,10 @@ export default class Store {
return !this.isInstalling && this.shouldInstall && (Date.now() > this.nextDisplay);
}
@action setExtensionActive = () => {
this.hasExtension = true;
}
@action setInstalling = (isInstalling) => {
this.isInstalling = isInstalling;
}
@ -61,6 +68,7 @@ export default class Store {
const ua = browser.analyze(navigator.userAgent || '');
if (hasExtension) {
this.setExtensionActive();
return false;
}
@ -97,4 +105,12 @@ export default class Store {
}
});
}
static get () {
if (!instance) {
instance = new Store();
}
return instance;
}
}

View File

@ -14,6 +14,7 @@
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.toolbar {
background: none !important;
height: 72px !important;
@ -53,7 +54,7 @@
.tabLink,
.settings,
.logo,
.first,
.last {
background: rgba(0, 0, 0, 0.5) !important; /* rgba(0, 0, 0, 0.25) !important; */
}
@ -73,27 +74,17 @@
right: -12px;
}
.logo {
margin: 0 0 0 -24px;
padding: 20px 24px;
white-space: nowrap;
}
.logo img {
height: 28px;
margin-right: 0.75em;
}
.logo div {
display: inline-block;
text-transform: uppercase;
line-height: 32px;
vertical-align: top;
letter-spacing: 0.2em;
}
.first,
.last {
margin: 0 -24px 0 0;
margin: 0;
padding: 36px 12px;
white-space: nowrap;
}
.first {
margin-left: -24px;
}
.last {
margin-right: -24px;
}

View File

@ -20,13 +20,16 @@ import { Link } from 'react-router';
import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar';
import { isEqual } from 'lodash';
import imagesEthcoreBlock from '~/../assets/images/parity-logo-white-no-text.svg';
import { Tooltip } from '~/ui';
import Tab from './Tab';
import styles from './tabBar.css';
class TabBar extends Component {
static contextTypes = {
router: PropTypes.object.isRequired
};
static propTypes = {
isTest: PropTypes.bool,
netChain: PropTypes.string,
@ -41,72 +44,48 @@ class TabBar extends Component {
render () {
return (
<Toolbar className={ styles.toolbar }>
{ this.renderLogo() }
{ this.renderTabs() }
{ this.renderLast() }
<ToolbarGroup className={ styles.first }>
<div />
</ToolbarGroup>
<div className={ styles.tabs }>
{ this.renderTabItems() }
</div>
<ToolbarGroup className={ styles.last }>
<div />
</ToolbarGroup>
</Toolbar>
);
}
renderLogo () {
return (
<ToolbarGroup>
<div className={ styles.logo }>
<img
height={ 28 }
src={ imagesEthcoreBlock }
/>
</div>
</ToolbarGroup>
);
}
renderLast () {
return (
<ToolbarGroup>
<div className={ styles.last }>
<div />
</div>
</ToolbarGroup>
);
}
renderTabs () {
renderTabItems () {
const { views, pending } = this.props;
const items = views
.map((view, index) => {
const body = (view.id === 'accounts')
? (
<Tooltip
className={ styles.tabbarTooltip }
text='navigate between the different parts and views of the application, switching between an account view, token view and distributed application view'
/>
)
: null;
return views.map((view, index) => {
const body = (view.id === 'accounts')
? (
<Tooltip
className={ styles.tabbarTooltip }
text='navigate between the different parts and views of the application, switching between an account view, token view and distributed application view'
/>
)
: null;
return (
<Link
activeClassName={ styles.tabactive }
className={ styles.tabLink }
key={ view.id }
to={ view.route }
return (
<Link
activeClassName={ styles.tabactive }
className={ styles.tabLink }
key={ view.id }
to={ view.route }
>
<Tab
pendings={ pending.length }
view={ view }
>
<Tab
pendings={ pending.length }
view={ view }
>
{ body }
</Tab>
</Link>
);
});
return (
<div className={ styles.tabs }>
{ items }
</div>
);
{ body }
</Tab>
</Link>
);
});
}
}

View File

@ -1,99 +0,0 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { withRouter } from 'react-router';
import Button from '~/ui/Button';
import { LinkIcon } from '~/ui/Icons';
import Input from '~/ui/Form/Input';
import styles from './urlButton.css';
const INPUT_STYLE = { display: 'inline-block', width: '20em' };
class UrlButton extends Component {
static propTypes = {
router: PropTypes.object.isRequired // injected by withRouter
};
state = {
inputShown: false
};
render () {
const { inputShown } = this.state;
return (
<div>
{ inputShown ? this.renderInput() : null }
<Button
className={ styles.button }
icon={ <LinkIcon /> }
label={
<FormattedMessage
id='dapps.button.url.label'
defaultMessage='URL'
/>
}
onClick={ this.toggleInput }
/>
</div>
);
}
renderInput () {
return (
<Input
hint={
<FormattedMessage
id='dapps.button.url.input'
defaultMessage='https://mkr.market'
/>
}
onBlur={ this.hideInput }
onFocus={ this.showInput }
onSubmit={ this.inputOnSubmit }
style={ INPUT_STYLE }
/>
);
}
toggleInput = () => {
const { inputShown } = this.state;
this.setState({
inputShown: !inputShown
});
}
hideInput = () => {
this.setState({ inputShown: false });
}
showInput = () => {
this.setState({ inputShown: true });
}
inputOnSubmit = (url) => {
const { router } = this.props;
router.push(`/web/${encodeURIComponent(url)}`);
}
}
export default withRouter(UrlButton);

View File

@ -19,18 +19,20 @@
flex-wrap: wrap;
margin: -0.125em;
position: relative;
.item {
box-sizing: border-box;
flex: 0 1 50%;
opacity: 0.85;
padding: 0.125em;
}
}
.list+.list {
margin-top: -0.25em;
}
.item {
padding: 0.125em;
flex: 0 1 50%;
box-sizing: border-box;
}
.overlay {
background: rgba(0, 0, 0, 0.85);
bottom: 0.5em;

View File

@ -26,7 +26,6 @@ import PermissionStore from '~/modals/DappPermissions/store';
import { Actionbar, Button, DappCard, Page } from '~/ui';
import { LockedIcon, VisibleIcon } from '~/ui/Icons';
import UrlButton from './UrlButton';
import DappsStore from './dappsStore';
import styles from './dapps.css';
@ -92,7 +91,6 @@ class Dapps extends Component {
/>
}
buttons={ [
<UrlButton key='url' />,
<Button
icon={ <VisibleIcon /> }
key='edit'

View File

@ -0,0 +1,50 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.accounts {
margin-top: 1.5em;
text-align: center;
.account {
position: relative;
line-height: 2em;
vertical-align: middle;
white-space: nowrap;
.icon {
margin: 0;
}
.link, .name {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
.name {
white-space: nowrap;
}
.timestamp {
color: #aaa;
font-size: 0.75em;
line-height: 1em;
padding-top: 0;
white-space: normal;
}
}
}

View File

@ -0,0 +1,139 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import moment from 'moment';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import { Container, ContainerTitle, IdentityName, IdentityIcon, SectionList } from '~/ui';
import { arrayOrObjectProptype } from '~/util/proptypes';
import styles from './accounts.css';
class Accounts extends Component {
static propTypes = {
accountsInfo: PropTypes.object,
history: arrayOrObjectProptype().isRequired
};
render () {
return (
<div className={ styles.accounts }>
<ContainerTitle
title={
<FormattedMessage
id='home.accounts.title'
defaultMessage='Recent Accounts'
/>
}
/>
{ this.renderHistory() }
</div>
);
}
renderHistory () {
const { accountsInfo, history } = this.props;
if (!accountsInfo || !Object.keys(accountsInfo).length) {
return null;
}
if (!history.length) {
return (
<div className={ styles.empty }>
<FormattedMessage
id='home.accounts.none'
defaultMessage='No recent accounts history available'
/>
</div>
);
}
return (
<SectionList
items={ history }
renderItem={ this.renderHistoryItem }
/>
);
}
renderHistoryItem = (history) => {
const { accountsInfo } = this.props;
if (!history || !history.entry) {
return null;
}
const account = accountsInfo[history.entry] || { meta: {} };
let linkType = 'addresses';
if (account.uuid) {
linkType = 'accounts';
} else if (account.meta.wallet) {
linkType = 'wallet';
}
return (
<Container
className={ styles.account }
key={ history.timestamp }
hover={
<div className={ styles.timestamp }>
<FormattedMessage
id='home.account.visited'
defaultMessage='accessed {when}'
values={ {
when: moment(history.timestamp).fromNow()
} }
/>
</div>
}
>
<Link
className={ styles.link }
to={ `/${linkType}/${history.entry}` }
>
<IdentityIcon
address={ history.entry }
className={ styles.icon }
center
/>
<IdentityName
address={ history.entry }
className={ styles.name }
unknown
/>
</Link>
</Container>
);
}
}
function mapStateToProps (state) {
const { accountsInfo } = state.personal;
return {
accountsInfo
};
}
export default connect(
mapStateToProps,
null
)(Accounts);

View File

@ -0,0 +1,71 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import Accounts from './';
let component;
let store;
function createRedux () {
store = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
personal: {
accountsInfo: { '0x123': {} }
}
};
}
};
return store;
}
function render (history = []) {
component = shallow(
<Accounts
history={ history }
/>,
{
context: {
store: createRedux()
}
}
).find('Accounts').shallow();
return component;
}
describe('views/Home/Accounts', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
describe('no history', () => {
beforeEach(() => {
render();
});
it('renders empty message', () => {
expect(component.find('FormattedMessage').props().id).to.equal('home.accounts.none');
});
});
});

View File

@ -14,4 +14,4 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './urlButton';
export default from './accounts';

View File

@ -0,0 +1,89 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import moment from 'moment';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { Container, DappIcon } from '~/ui';
import styles from './dapps.css';
export default class Dapp extends Component {
static propTypes = {
id: PropTypes.string.isRequired,
store: PropTypes.object.isRequired,
timestamp: PropTypes.number.isRequired
}
state = {
dapp: null
}
componentWillMount () {
return this.loadApp();
}
render () {
const { id, timestamp } = this.props;
const { dapp } = this.state;
if (!dapp) {
return null;
}
return (
<Container
className={ styles.dapp }
hover={
<div className={ styles.timestamp }>
<FormattedMessage
id='home.dapp.visited'
defaultMessage='accessed {when}'
values={ {
when: moment(timestamp).fromNow()
} }
/>
</div>
}
>
<Link
className={ styles.link }
to={ `/app/${id}` }
>
<DappIcon
app={ dapp }
className={ styles.icon }
/>
<span className={ styles.name }>
{ dapp.name }
</span>
</Link>
</Container>
);
}
loadApp = () => {
const { id, store } = this.props;
return store
.loadApp(id)
.then((dapp) => {
this.setState({ dapp });
});
}
}

View File

@ -0,0 +1,55 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import Dapp from './dapp';
import { createStore } from './dapps.test.js';
let component;
let instance;
let store;
function render () {
store = createStore();
component = shallow(
<Dapp
id='testId'
store={ store }
timestamp={ Date.now() }
/>
);
instance = component.instance();
return component;
}
describe('views/Home/Dapp', () => {
beforeEach(() => {
render();
return instance.componentWillMount();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('loads the dapp on mount', () => {
expect(store.loadApp).to.have.been.calledWith('testId');
});
});

View File

@ -0,0 +1,48 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.dapps {
margin-top: 1.5em;
text-align: center;
.dapp {
position: relative;
line-height: 2em;
vertical-align: middle;
.icon {
margin: 0;
}
.link, .name {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
.name {
white-space: nowrap;
}
.timestamp {
color: #aaa;
font-size: 0.75em;
line-height: 1em;
padding-top: 0;
}
}
}

View File

@ -0,0 +1,86 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { ContainerTitle, SectionList } from '~/ui';
import { arrayOrObjectProptype } from '~/util/proptypes';
import Dapp from './dapp';
import styles from './dapps.css';
export default class Dapps extends Component {
static propTypes = {
history: arrayOrObjectProptype().isRequired,
store: PropTypes.object.isRequired
}
render () {
return (
<div className={ styles.dapps }>
<ContainerTitle
title={
<FormattedMessage
id='home.dapps.title'
defaultMessage='Recent Dapps'
/>
}
/>
{ this.renderHistory() }
</div>
);
}
renderHistory () {
const { history } = this.props;
if (!history.length) {
return (
<div className={ styles.empty }>
<FormattedMessage
id='home.dapps.none'
defaultMessage='No recent Applications history available'
/>
</div>
);
}
return (
<SectionList
items={ history }
renderItem={ this.renderHistoryItem }
/>
);
}
renderHistoryItem = (history) => {
if (!history || !history.entry) {
return null;
}
const { store } = this.props;
return (
<Dapp
id={ history.entry }
key={ history.timestamp }
store={ store }
timestamp={ history.timestamp }
/>
);
}
}

View File

@ -0,0 +1,68 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import Dapps from './';
import { createStore } from './dapps.test.js';
let component;
let store;
function render (history = []) {
store = createStore();
component = shallow(
<Dapps
history={ history }
store={ store }
/>
);
return component;
}
describe('views/Home/Dapps', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
describe('no history', () => {
beforeEach(() => {
render();
});
it('renders empty message', () => {
expect(component.find('FormattedMessage').props().id).to.equal('home.dapps.none');
});
});
describe('with history', () => {
const HISTORY = [
{ timestamp: 1, entry: 'testABC' },
{ timestamp: 2, entry: 'testDEF' }
];
beforeEach(() => {
render(HISTORY);
});
it('renders SectionList', () => {
expect(component.find('SectionList').length).to.equal(1);
});
});
});

View File

@ -0,0 +1,27 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
function createStore () {
return {
loadApp: sinon.stub().resolves({ name: 'testName' })
};
}
export {
createStore
};

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './dapps';

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './news';

View File

@ -0,0 +1,73 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.news {
}
.markdown {
line-height: 1.2em;
}
.item {
height: 240px;
opacity: 0.85;
position: relative;
width: 100%;
.background {
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: cover;
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
width: auto;
}
.overlay {
background: white;
color: #333;
display: none;
left: 0;
position: absolute;
right: 0;
top: 100%;
padding: 0 1.5em 1em 1.5em;
}
.title {
background: rgba(255, 255, 255, 0.85);
bottom: 0;
color: #333;
font-size: 1.17em;
left: 0;
padding: 1rem 1.5rem;
position: absolute;
right: 0;
text-transform: uppercase;
}
&:hover {
opacity: 1;
.overlay {
display: block;
}
}
}

View File

@ -0,0 +1,92 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component } from 'react';
import ReactMarkdown from 'react-markdown';
import { SectionList } from '~/ui';
import { createRenderers } from './renderers';
import Store from './store';
import styles from './news.css';
const VERSION_ID = '1';
@observer
export default class News extends Component {
store = Store.get();
componentWillMount () {
return this.store.retrieveNews(VERSION_ID);
}
render () {
const { newsItems } = this.store;
if (!newsItems || !newsItems.length) {
return null;
}
return (
<SectionList
className={ styles.news }
items={ newsItems }
renderItem={ this.renderItem }
/>
);
}
renderItem = (item) => {
if (!item) {
return null;
}
const inlineStyles = item.style || {};
return (
<div className={ styles.item }>
<div
className={ styles.background }
style={ {
backgroundImage: `url(${item.background})`
} }
/>
<div
className={ styles.title }
style={ inlineStyles.head }
>
{ item.title }
</div>
<div
className={ styles.overlay }
style={ inlineStyles.body }
>
<ReactMarkdown
className={ styles.markdown }
renderers={ createRenderers(inlineStyles.tags) }
source={ item.markdown }
softBreak='br'
/>
</div>
</div>
);
}
}
export {
VERSION_ID
};

View File

@ -0,0 +1,54 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import News from './news';
import { restoreGlobals, stubGlobals } from './news.test.js';
let component;
let instance;
function render () {
component = shallow(
<News />
);
instance = component.instance();
return component;
}
describe('views/Home/News', () => {
beforeEach(() => {
stubGlobals();
render();
return instance.componentWillMount();
});
afterEach(() => {
restoreGlobals();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('retrieves the content meta on mount', () => {
expect(instance.store.newsItems).to.equal('testContent');
});
});

View File

@ -0,0 +1,54 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
import Contracts from '~/contracts';
import { VERSION_ID } from './news';
let contracts;
let globalContractsGet;
let globalFetch;
export function stubGlobals () {
contracts = {
githubHint: {
getEntry: sinon.stub().resolves(['testUrl', 'testOwner', 'testCommit'])
},
registry: {
lookupMeta: sinon.stub().resolves('testMeta')
}
};
globalContractsGet = Contracts.get;
globalFetch = global.fetch;
sinon.stub(Contracts, 'get', () => contracts);
sinon.stub(global, 'fetch').resolves({
ok: true,
json: sinon.stub().resolves({
[VERSION_ID]: {
items: 'testContent'
}
})
});
}
export function restoreGlobals () {
Contracts.get = globalContractsGet;
global.fetch = globalFetch;
}

View File

@ -0,0 +1,37 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { createElement } from 'react';
export function createRenderers (tagStyles = {}) {
return Object
.keys(tagStyles)
.reduce((renderers, tag) => {
switch (tag) {
case 'a':
case 'link':
renderers['link'] = (mdProps) => {
const { children, href, title } = mdProps;
const style = tagStyles[tag];
return createElement('a', { href, title, style }, children);
};
break;
}
return renderers;
}, {});
}

View File

@ -0,0 +1,67 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { action, observable } from 'mobx';
import Contracts from '~/contracts';
let instance = null;
export default class Store {
@observable newsItems = null;
@action setNewsItems = (newsItems) => {
this.newsItems = newsItems;
}
retrieveNews (versionId) {
const contracts = Contracts.get();
return contracts.registry
.lookupMeta('paritynews', 'CONTENT')
.then((contentId) => {
return contracts.githubHint.getEntry(contentId);
})
.then(([url, owner, commit]) => {
if (!url) {
return null;
}
return fetch(url).then((response) => {
if (!response.ok) {
return null;
}
return response.json();
});
})
.then((news) => {
if (news && news[versionId]) {
this.setNewsItems(news[versionId].items);
}
})
.catch((error) => {
console.warn('retrieveNews', error);
});
}
static get () {
if (!instance) {
instance = new Store();
}
return instance;
}
}

View File

@ -0,0 +1,57 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { VERSION_ID } from './news';
import { restoreGlobals, stubGlobals } from './news.test.js';
import Store from './store';
let store;
function create () {
store = new Store();
return store;
}
describe('views/Home/News/Store', () => {
beforeEach(() => {
stubGlobals();
create();
});
afterEach(() => {
restoreGlobals();
});
describe('@action', () => {
describe('setNewsItems', () => {
it('sets the items', () => {
store.setNewsItems('testing');
expect(store.newsItems).to.equal('testing');
});
});
});
describe('operations', () => {
describe('retrieveNews', () => {
it('retrieves the items', () => {
return store.retrieveNews(VERSION_ID).then(() => {
expect(store.newsItems).to.equal('testContent');
});
});
});
});
});

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './urls';

View File

@ -0,0 +1,80 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.urls {
text-align: center;
.layout {
box-sizing: border-box;
margin: 1.5em auto;
padding: 0 1em;
width: 50%;
.empty {
margin-top: 0.5em;
opacity: 0.75;
}
.historyItem {
height: auto;
margin-top: 1em;
position: relative;
width: 100%;
.linkIcon {
opacity: 0;
position: absolute;
right: 0.5em;
top: 0.5em;
}
.url {
display: block;
color: rgb(0, 151, 167);
overflow: hidden;
text-overflow: ellipsis;
}
.timestamp {
color: #aaa;
font-size: 0.75em;
line-height: 1em;
padding-top: 0.5rem;
}
&:hover {
opacity: 1;
.linkIcon {
opacity: 1;
}
}
}
.input {
background: rgba(255, 255, 255, 0.25);
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 0.25em;
box-sizing: border-box;
color: white;
display: block;
font-size: 1.25em;
padding: 0.5em;
width: 100%;
}
}
}

View File

@ -0,0 +1,139 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import moment from 'moment';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Container, ContainerTitle, DappUrlInput, SectionList } from '~/ui';
import { LinkIcon } from '~/ui/Icons';
import styles from './urls.css';
@observer
export default class Urls extends Component {
static contextTypes = {
router: PropTypes.object.isRequired
};
static propTypes = {
extensionStore: PropTypes.object.isRequired,
store: PropTypes.object.isRequired
}
render () {
const { nextUrl } = this.props.store;
return (
<div className={ styles.urls }>
<div className={ styles.layout }>
<ContainerTitle
title={
<FormattedMessage
id='home.url.title'
defaultMessage='Web Applications'
/>
}
/>
<DappUrlInput
className={ styles.input }
onChange={ this.onChangeUrl }
onGoto={ this.onGotoUrl }
onRestore={ this.onRestoreUrl }
url={ nextUrl }
/>
{ this.renderHistory() }
</div>
</div>
);
}
renderHistory () {
const { history } = this.props.store;
if (!history.length) {
return (
<div className={ styles.empty }>
<FormattedMessage
id='home.url.none'
defaultMessage='No recent URL history available'
/>
</div>
);
}
return (
<SectionList
items={ history }
renderItem={ this.renderHistoryItem }
/>
);
}
renderHistoryItem = (history) => {
if (!history || !history.url) {
return null;
}
const onNavigate = () => this.onGotoUrl(history.url);
return (
<Container
className={ styles.historyItem }
hover={
<div className={ styles.timestamp }>
<FormattedMessage
id='home.url.visited'
defaultMessage='visited {when}'
values={ {
when: moment(history.timestamp).fromNow()
} }
/>
</div>
}
key={ history.timestamp }
onClick={ onNavigate }
>
<LinkIcon className={ styles.linkIcon } />
<div className={ styles.url }>
{ history.hostname }
</div>
</Container>
);
}
onChangeUrl = (url) => {
this.props.store.setNextUrl(url);
}
onGotoUrl = (url) => {
const { router } = this.context;
const { extensionStore } = this.props;
this.props.store.gotoUrl(url);
if (extensionStore.hasExtension) {
window.open(this.props.store.currentUrl, '_blank');
} else {
router.push('/web');
}
}
onRestoreUrl = () => {
this.props.store.restoreUrl();
}
}

View File

@ -0,0 +1,124 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import Urls from './';
const NEXT_URL = 'http://somewhere.next';
let component;
let instance;
let router;
let store;
function createRouter () {
router = {
push: sinon.stub()
};
return router;
}
function createStore () {
store = {
history: [],
gotoUrl: sinon.stub(),
restoreUrl: sinon.stub(),
setNextUrl: sinon.stub(),
nextUrl: NEXT_URL
};
return store;
}
function render () {
component = shallow(
<Urls
extensionStore={ { hasExtension: false } }
store={ createStore() }
/>,
{
context: {
router: createRouter()
}
}
);
instance = component.instance();
return component;
}
describe('views/Home/Urls', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('input', () => {
let input;
beforeEach(() => {
input = component.find('DappUrlInput');
});
it('renders the input cmponent', () => {
expect(input.length).to.equal(1);
});
it('passes nextUrl as url', () => {
expect(input.props().url).to.equal(NEXT_URL);
});
});
describe('events', () => {
describe('onChangeUrl', () => {
it('performs setNextUrl on store', () => {
instance.onChangeUrl('123');
expect(store.setNextUrl).to.have.been.calledWith('123');
});
});
describe('onGotoUrl', () => {
it('performs gotoUrl on store', () => {
instance.onGotoUrl();
expect(store.gotoUrl).to.have.been.called;
});
it('passed the URL when provided', () => {
instance.onGotoUrl('http://example.com');
expect(store.gotoUrl).to.have.been.calledWith('http://example.com');
});
it('does route navigation when executed', () => {
instance.onGotoUrl();
expect(router.push).to.have.been.calledWith('/web');
});
});
describe('onRestoreUrl', () => {
it('performs restoreUrl on store', () => {
instance.onRestoreUrl();
expect(store.restoreUrl).to.have.been.called;
});
});
});
});

View File

@ -15,6 +15,26 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.button {
vertical-align: middle;
.accounts {
margin-top: 1.5em;
}
.body {
padding-bottom: 3em;
}
.empty {
margin-top: 1.5em;
opacity: 0.5;
}
.row {
display: flex;
.column {
box-sizing: border-box;
flex: 0 1 50%;
padding: 0 1.5em;
width: 50%;
}
}

81
js/src/views/Home/home.js Normal file
View File

@ -0,0 +1,81 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Page } from '~/ui';
import DappsStore from '../Dapps/dappsStore';
import ExtensionStore from '../Application/Extension/store';
import HistoryStore from '../historyStore';
import WebStore from '../Web/store';
import Accounts from './Accounts';
import Dapps from './Dapps';
import News from './News';
import Urls from './Urls';
import styles from './home.css';
@observer
export default class Home extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
dappsStore = DappsStore.get(this.context.api);
extensionStore = ExtensionStore.get();
webStore = WebStore.get(this.context.api);
accountsHistory = HistoryStore.get('accounts');
dappsHistory = HistoryStore.get('dapps');
componentWillMount () {
return this.webStore.loadHistory();
}
render () {
return (
<Page
className={ styles.body }
title={
<FormattedMessage
id='home.title'
defaultMessage='Parity Home'
/>
}
>
<News />
<Urls
extensionStore={ this.extensionStore }
store={ this.webStore }
/>
<div className={ styles.row }>
<div className={ styles.column }>
<Dapps
history={ this.dappsHistory.history }
store={ this.dappsStore }
/>
</div>
<div className={ styles.column }>
<Accounts history={ this.accountsHistory.history } />
</div>
</div>
</Page>
);
}
}

View File

@ -0,0 +1,96 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import Home from './';
const TEST_APP_HISTORY = [];
let api;
let component;
let instance;
function createApi () {
api = {
parity: {
listRecentDapps: sinon.stub().resolves(TEST_APP_HISTORY)
}
};
return api;
}
function render () {
component = shallow(
<Home />,
{
context: {
api: createApi()
}
}
);
instance = component.instance();
return component;
}
describe('views/Home', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('lifecycle', () => {
describe('componentWillMount', () => {
beforeEach(() => {
sinon.stub(instance.webStore, 'loadHistory');
return instance.componentWillMount();
});
afterEach(() => {
instance.webStore.loadHistory.restore();
});
it('calls into webStore loadHistory', () => {
expect(instance.webStore.loadHistory).to.have.been.called;
});
});
});
describe('components', () => {
it('renders Accounts', () => {
expect(component.find('Connect(Accounts)').length).to.equal(1);
});
it('renders Dapps', () => {
expect(component.find('Dapps').length).to.equal(1);
});
it('renders News', () => {
expect(component.find('News').length).to.equal(1);
});
it('renders Urls', () => {
expect(component.find('Urls').length).to.equal(1);
});
});
});

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './home';

View File

@ -19,20 +19,7 @@ $overlayZ: 10000;
$modalZ: 10001;
.account {
display: flex;
flex: 1;
overflow: hidden;
position: relative;
.accountOverlay {
position: absolute;
right: 0.5em;
top: 0.5em;
}
.iconDisabled {
opacity: 0.15;
}
width: 100%;
.selected,
.unselected {

View File

@ -23,7 +23,24 @@ import CommunicationContacts from 'material-ui/svg-icons/communication/contacts'
import ImageGridOn from 'material-ui/svg-icons/image/grid-on';
import NavigationApps from 'material-ui/svg-icons/navigation/apps';
import imagesEthcoreBlock from '~/../assets/images/parity-logo-white-no-text.svg';
import styles from './views.css';
const defaultViews = {
home: {
active: true,
fixed: true,
icon: (
<img
className={ styles.logoIcon }
src={ imagesEthcoreBlock }
/>
),
route: '/home',
value: 'home'
},
accounts: {
active: true,
fixed: true,

View File

@ -43,6 +43,12 @@
color: white !important;
}
.logoIcon {
height: 24px;
margin-bottom: 5px;
opacity: 0.5;
}
.info {
color: #aaa;
padding-left: 4.75em;

View File

@ -82,8 +82,31 @@ export default class Store {
this.setNextUrl(this.currentUrl);
}
@action setHistory = (history) => {
this.history = history;
@action setHistory = (urls) => {
this.history = Object
.keys(urls)
.filter((url) => url && !url.startsWith(this._api.dappsUrl) && url.indexOf('127.0.0.1') === -1)
.sort((urlA, urlB) => {
const timeA = urls[urlA].getTime();
const timeB = urls[urlB].getTime();
if (timeA > timeB) {
return -1;
} else if (timeA < timeB) {
return 1;
}
return 0;
})
.map((url) => {
const hostname = url.replace(/^http[s]?:\/\//, '').split('/')[0];
return {
hostname,
timestamp: urls[url],
url
};
});
}
@action setLoading = (isLoading) => {

View File

@ -18,7 +18,13 @@ import sinon from 'sinon';
import Store from './store';
const TEST_HISTORY = ['somethingA', 'somethingB'];
const TEST_HISTORY_URLA = 'http://testingA';
const TEST_HISTORY_URLB = 'http://testingB';
const TEST_HISTORY = {
'': new Date(678),
[TEST_HISTORY_URLA]: new Date(123),
[TEST_HISTORY_URLB]: new Date(456)
};
const TEST_TOKEN = 'testing-123';
const TEST_URL1 = 'http://some.test.domain.com';
const TEST_URL2 = 'http://something.different.com';
@ -89,9 +95,28 @@ describe('views/Web/Store', () => {
});
describe('setHistory', () => {
it('sets the history', () => {
let history;
beforeEach(() => {
store.setHistory(TEST_HISTORY);
expect(store.history.peek()).to.deep.equal(TEST_HISTORY);
history = store.history.peek();
});
it('sets the history', () => {
expect(history.length).to.equal(2);
});
it('adds hostname to entries', () => {
expect(history[1].hostname).to.be.ok;
});
it('removes hostname http prefixes', () => {
expect(history[1].hostname.indexOf('http')).to.equal(-1);
});
it('sorts the entries according to recently accessed', () => {
expect(history[0].url).to.equal(TEST_HISTORY_URLB);
expect(history[1].url).to.equal(TEST_HISTORY_URLA);
});
});
@ -195,7 +220,7 @@ describe('views/Web/Store', () => {
});
it('sets the history as retrieved', () => {
expect(store.history.peek()).to.deep.equal(TEST_HISTORY);
expect(store.history.peek().length).not.to.equal(0);
});
});
});

View File

@ -30,10 +30,11 @@ export default class Store {
this.load();
}
@action add = (entry) => {
@action add = (entry, type) => {
this.history = [{
entry,
timestamp: Date.now(),
entry
type
}].concat(this.history.filter((h) => h.entry !== entry)).slice(0, MAX_ENTRIES);
this.save();
}

View File

@ -24,6 +24,7 @@ export Contracts from './Contracts';
export Dapp from './Dapp';
export Dapps from './Dapps';
export HistoryStore from './historyStore';
export Home from './Home';
export ParityBar from './ParityBar';
export Settings, { SettingsBackground, SettingsParity, SettingsProxy, SettingsViews } from './Settings';
export Signer from './Signer';