ENS Search Styling (#1018)

* Restyled, recopied, and did some component refactoring for ENS.

* Awkward copy fix.

* Update snapshot.

* Overflow table handling.

* Re-enable on error.
This commit is contained in:
William O'Beirne 2018-02-08 12:30:30 -05:00 committed by Daniel Ternyak
parent df4b73721a
commit 7ac546acaf
21 changed files with 303 additions and 265 deletions

View File

@ -63,7 +63,7 @@ export default class Root extends Component<Props, State> {
<Route path="/generate" component={GenerateWallet} />
<Route path="/swap" component={Swap} />
<Route path="/contracts" component={Contracts} />
<Route path="/ens" component={ENS} />
<Route path="/ens" component={ENS} exact={true} />
<Route path="/sign-and-verify-message" component={SignAndVerifyMessage} />
<Route path="/pushTx" component={BroadcastTx} />
<RouteNotFound />

View File

@ -31,8 +31,8 @@ export interface AAttributes {
interface NewTabLinkProps extends AAttributes {
href: string;
content?: React.ReactElement<any> | string;
children?: React.ReactElement<any> | string;
content?: React.ReactElement<any> | string | string[];
children?: React.ReactElement<any> | string | string[];
}
export class NewTabLink extends React.Component<NewTabLinkProps> {

View File

@ -94,7 +94,7 @@ const GeneralInfoList = () => (
</section>
);
export const GeneralInfoPanel = () => (
const GeneralInfoPanel = () => (
<article className="block">
<div className="cont-md">
<h4> What is the process like? </h4>
@ -113,3 +113,5 @@ export const GeneralInfoPanel = () => (
</div>
</article>
);
export default GeneralInfoPanel;

View File

@ -0,0 +1,17 @@
@import 'common/sass/variables';
.ENSInput {
max-width: 520px;
padding: $space-sm 0;
margin: 0 auto;
&-name {
margin-bottom: $space-md;
}
&-button {
.Spinner {
margin-left: $space-md;
}
}
}

View File

@ -0,0 +1,96 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { resolveDomainRequested, TResolveDomainRequested } from 'actions/ens';
import { isValidENSName } from 'libs/validators';
import './NameInput.scss';
interface State {
domainToCheck: string;
isValidDomain: boolean;
isFocused: boolean;
}
interface Props {
domainRequests: AppState['ens']['domainRequests'];
resolveDomainRequested: TResolveDomainRequested;
}
class NameInput extends Component<Props, State> {
public state = {
isFocused: false,
isValidDomain: false,
domainToCheck: ''
};
public render() {
const { domainRequests } = this.props;
const { isValidDomain, domainToCheck, isFocused } = this.state;
const req = domainRequests[domainToCheck];
const isLoading = req && !req.data && !req.error;
return (
<form className="ENSInput" onSubmit={this.onSubmit}>
<div className="ENSInput-name input-group">
<input
value={domainToCheck}
className={classnames(
'form-control',
!domainToCheck ? '' : isValidDomain ? 'is-valid' : 'is-invalid'
)}
type="text"
placeholder="mycrypto"
onChange={this.onChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
disabled={isLoading}
/>
<span className="input-group-addon">.eth</span>
</div>
{domainToCheck &&
!isValidDomain &&
!isFocused && (
<p className="help-block is-invalid">
Must be at least 7 characters, no special characters
</p>
)}
<button
className="ENSInput-button btn btn-primary btn-block"
disabled={!isValidDomain || isLoading}
>
Check Availability
</button>
</form>
);
}
// add delay to namehash computation / getting the availability
private onChange = (event: React.FormEvent<HTMLButtonElement>) => {
const domainToCheck = event.currentTarget.value.toLowerCase().trim();
const isValidDomain = isValidENSName(domainToCheck);
this.setState({
domainToCheck,
isValidDomain
});
};
private onSubmit = (ev: React.FormEvent<HTMLElement>) => {
ev.preventDefault();
const { isValidDomain, domainToCheck } = this.state;
return isValidDomain && this.props.resolveDomainRequested(domainToCheck);
};
private onFocus = () => this.setState({ isFocused: true });
private onBlur = () => this.setState({ isFocused: false });
}
function mapStateToProps(state: AppState) {
return {
domainRequests: state.ens.domainRequests
};
}
export default connect(mapStateToProps, {
resolveDomainRequested
})(NameInput);

View File

@ -1,39 +0,0 @@
import React from 'react';
import NameInputHoc from './NameInputHOC';
interface Props {
isValidDomain: boolean;
domainToCheck: string;
onClick(ev: React.FormEvent<HTMLButtonElement>): void;
onChange(ev: React.FormEvent<HTMLInputElement>): void;
}
class ENSNameInput extends React.Component<Props, {}> {
public render() {
const { onChange, onClick, isValidDomain, domainToCheck } = this.props;
return (
<article className="row">
<section className="col-xs-12 col-sm-6 col-sm-offset-3 text-center">
<div className="input-group">
<input
className={`form-control ${
domainToCheck === '' ? '' : isValidDomain ? 'is-valid' : 'is-invalid'
}`}
type="text"
placeholder="myetherwallet"
onChange={onChange}
/>
<div className="input-group-btn">
<a className="btn btn-default">.eth</a>
</div>
</div>
{isValidDomain ? null : <p>Use at least 7 characters</p>}
<button className="btn btn-primary " onClick={onClick}>
Check ENS Name
</button>
</section>
</article>
);
}
}
export default NameInputHoc(ENSNameInput);

View File

@ -1,42 +0,0 @@
import React, { Component } from 'react';
import { isValidENSName } from 'libs/validators';
interface State {
domainToCheck: string;
isValidDomain: boolean;
}
interface Props {
resolveDomainRequested(domain: string): void;
}
const NameInputHoc = PassedComponent =>
class HOC extends Component<Props, State> {
public state = {
isValidDomain: false,
domainToCheck: ''
};
//add delay to namehash computation / getting the availability
public onChange = (event: React.FormEvent<HTMLButtonElement>) => {
const domainToCheck: string = event.currentTarget.value.toLowerCase();
this.setState({ domainToCheck });
const isValidName: boolean = isValidENSName(domainToCheck);
this.setState({ isValidDomain: isValidName });
};
public onClick = () => {
const { isValidDomain, domainToCheck } = this.state;
const { resolveDomainRequested } = this.props;
return isValidDomain && resolveDomainRequested(domainToCheck);
};
public render() {
const { onChange, onClick } = this;
const { isValidDomain, domainToCheck } = this.state;
const props = {
onChange,
onClick,
isValidDomain,
domainToCheck
};
return <PassedComponent {...props} />;
}
};
export default NameInputHoc;

View File

@ -1 +0,0 @@
export { default as NameInput } from './components/NameInput';

View File

@ -1,19 +1,30 @@
.auction-info {
margin-bottom: 32px;
@import 'common/sass/variables';
.NameResolve {
&-loader {
text-align: center;
padding: 4rem;
}
}
.ens-title {
margin: 2rem 0;
margin: 0 0 1.5rem;
h2 {
margin: 0;
line-height: 2.8rem;
}
}
.ens-panel-wrapper {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 2rem;
.ens-panel {
flex-grow: 1;
max-width: 540px;
min-width: 360px;
color: white;
padding: 1rem;
background-color: #185475;
@ -23,14 +34,14 @@
margin: 0;
}
&-light {
&.is-light {
background-color: #1e92ba;
}
}
}
.table-wrapper {
overflow-y: scroll;
.ens-table-wrapper {
overflow: auto;
}
@media (max-width: 820px) {

View File

@ -3,6 +3,7 @@ import { IBaseDomainRequest } from 'libs/ens';
import ENSTime from './components/ENSTime';
import moment from 'moment';
import { NewTabLink } from 'components/ui';
import { ensV3Url } from 'utils/formatters';
const getDeadlines = (registrationDate: string) => {
// Get the time to reveal bids, and the time when the action closes
@ -28,15 +29,19 @@ export const NameAuction: React.SFC<IBaseDomainRequest> = props => {
<section className="ens-panel">
<ENSTime text="Reveal Bids On" time={revealBidTime} />
</section>
<section className="ens-panel ens-panel-light">
<section className="ens-panel is-light">
<ENSTime text="Auction Closes On" time={auctionCloseTime} />
</section>
</div>
<NewTabLink
content={`Do you want to place a bid on ${name}.eth? You'll need to bid on MyCrypto V3 by clicking here: `}
href="https://mycrypto.com/#ens"
/>
<p>
Do you want to place a bid on {name}.eth?{' '}
<strong>
<NewTabLink href={ensV3Url(name)}>
You can do that on MyCrypto V3 by clicking here!
</NewTabLink>
</strong>
</p>
</div>
</section>
);

View File

@ -1,23 +1,25 @@
import React from 'react';
import { IBaseDomainRequest } from 'libs/ens';
import { NewTabLink } from 'components/ui';
import { ensV3Url } from 'utils/formatters';
export const NameOpen: React.SFC<IBaseDomainRequest> = props => (
<section className="row">
<section className="auction-info text-center">
<div className="ens-title">
<h1>
<strong>{props.name}.eth</strong> is available!
<strong>{props.name}.eth</strong> is available
</h1>
</div>
<NewTabLink
className="text-center"
content={`Do you want ${
props.name
}.eth? You'll need open an auction on MyCrypto V3 by clicking here`}
href="https://mycrypto.com/#ens"
/>
<p>
Do you want {props.name}.eth?{' '}
<strong>
<NewTabLink className="text-center" href={ensV3Url(props.name)}>
Open an auction on MyCrypto v3!
</NewTabLink>
</strong>
</p>
</section>
</section>
);

View File

@ -21,7 +21,7 @@ export const NameOwned: React.SFC<IOwnedDomainRequest> = ({
<strong>{name}.eth</strong> is already owned
</h1>
</div>
<div className="table-wrapper">
<div className="ens-table-wrapper">
<table className="table table-striped">
<tbody>
<tr>

View File

@ -3,27 +3,25 @@ import { IRevealDomainRequest } from 'libs/ens';
import ENSTime from './components/ENSTime';
import { UnitDisplay, NewTabLink } from 'components/ui';
import { Wei } from 'libs/units';
import { ensV3Url } from 'utils/formatters';
export const NameReveal: React.SFC<IRevealDomainRequest> = props => (
<section className="row text-center">
<div className="auction-info text-center">
<div className="ens-title">
<h2>
<p>
It's time to reveal the bids for <strong>{props.name}.eth.</strong>{' '}
</p>
<p>
Current Highest bid is{' '}
<strong>
<UnitDisplay
value={Wei(props.highestBid)}
unit="ether"
symbol="ETH"
displayShortBalance={false}
checkOffline={false}
/>
</strong>
</p>
It's time to reveal the bids for <strong>{props.name}.eth</strong>
<br />
The current highest bid is{' '}
<strong>
<UnitDisplay
value={Wei(props.highestBid)}
unit="ether"
symbol="ETH"
displayShortBalance={false}
checkOffline={false}
/>
</strong>
</h2>
</div>
@ -33,11 +31,14 @@ export const NameReveal: React.SFC<IRevealDomainRequest> = props => (
</section>
</div>
</div>
<NewTabLink
content={`Did you you bid on ${
props.name
}.eth? You must reveal your bid now. You'll need reveal your bid on MyCrypto V3 by clicking here`}
href="https://mycrypto.com/#ens"
/>
<p>
Did you bid on {props.name}.eth? You must reveal your bid now.{' '}
<strong>
<NewTabLink href={ensV3Url(props.name)}>
You can do that on MyCrypto v3 by clicking here!
</NewTabLink>
</strong>
</p>
</section>
);

View File

@ -6,53 +6,46 @@ interface Props {
}
interface State {
currentTime: number;
timeDisplay: string;
}
class CountDown extends Component<Props, State> {
public state = { currentTime: 0 };
public state = { timeDisplay: '' };
constructor(props: Props) {
super(props);
public componentDidMount() {
this.startCountDown();
this.state = { currentTime: 0 };
}
public render() {
const { currentTime } = this.state;
return <p>{this.humanizeTime(currentTime)}</p>;
return <p>{this.state.timeDisplay}</p>;
}
private humanizeTime = (time: number) => {
let timeRemaining = time;
const floorTime = unit => Math.floor(timeRemaining / unit);
const pad = (num: number) => num.toString().padStart(2, '0');
const second = 1000;
const minute = second * 60;
const hour = minute * 60;
const day = hour * 24;
const days = floorTime(day);
timeRemaining -= days * day;
const hours = floorTime(hour);
timeRemaining -= hours * hour;
const minutes = floorTime(minute);
timeRemaining -= minutes * minute;
const seconds = floorTime(second);
return `${pad(days)} Days ${pad(hours)} Hours ${pad(minutes)} Minutes ${pad(seconds)} Seconds `;
};
private startCountDown = () => {
const intervalId = window.setInterval(() => {
const nextTime = +moment(this.props.initialTime).diff(+moment(), 'ms');
const time = moment(this.props.initialTime);
let intervalId;
if (nextTime < 0) {
return clearInterval(intervalId);
const setTimeDisplay = () => {
const diff = moment.duration(time.diff(moment()));
let timeDisplay;
if (diff) {
const pieces = [
diff.days() > 0 && `${diff.days()} days`,
diff.hours() > 0 && `${diff.hours()} hours`,
diff.minutes() > 0 && `${diff.minutes()} minutes`,
diff.seconds() > 0 && `${diff.seconds()} seconds`
].filter(piece => !!piece);
timeDisplay = `in ${pieces.join(', ')}`;
} else {
clearInterval(intervalId);
timeDisplay = 'Auction is over!';
}
this.setState({ currentTime: nextTime });
}, 1000);
this.setState({ timeDisplay });
};
intervalId = setInterval(setTimeDisplay, 1000);
setTimeDisplay();
};
}
@ -62,9 +55,9 @@ interface ITime {
}
const ENSTime: React.SFC<ITime> = ({ text, time }) => (
<section className="sm-6 col-xs-12 order-info">
<section>
<p>{text}</p>
<h4>{moment(time).toString()}</h4>
<h4>{moment(time).format('LLLL')}</h4>
<CountDown initialTime={time} />
</section>
);

View File

@ -1,4 +1,5 @@
import React from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { NameState } from 'libs/ens';
import {
@ -9,8 +10,8 @@ import {
NameOpen,
NameReveal
} from './components';
import './NameResolve.scss';
import { Spinner } from 'components/ui';
import './NameResolve.scss';
type Props = AppState['ens'];
@ -23,7 +24,7 @@ const modeResult = {
[NameState.Reveal]: NameReveal
};
export const NameResolve: React.SFC<Props> = props => {
const NameResolve: React.SFC<Props> = props => {
const { domainRequests, domainSelector } = props;
const { currentDomain } = domainSelector;
@ -32,7 +33,20 @@ export const NameResolve: React.SFC<Props> = props => {
}
const domainData = domainRequests[currentDomain].data! || false;
const Component = domainData ? modeResult[domainData.mode] : Spinner;
let content;
return <Component {...domainData} />;
if (domainData) {
const Component = modeResult[domainData.mode];
content = <Component {...domainData} />;
} else {
content = (
<div className="NameResolve-loader">
<Spinner size="x3" />
</div>
);
}
return <div className="Tab-content-pane">{content}</div>;
};
export default connect((state: AppState): Props => ({ ...state.ens }))(NameResolve);

View File

@ -1,3 +1,3 @@
export { GeneralInfoPanel } from './GeneralInfoPanel';
export { NameInput } from './NameInput';
export { NameResolve } from './NameResolve';
export { default as GeneralInfoPanel } from './GeneralInfoPanel';
export { default as NameInput } from './NameInput';
export { default as NameResolve } from './NameResolve';

View File

@ -0,0 +1,12 @@
@import 'common/sass/variables';
.ENS {
&-title {
text-align: center;
}
&-description {
max-width: 800px;
margin: 0 auto $space;
}
}

View File

@ -1,14 +1,9 @@
import React from 'react';
import { GeneralInfoPanel, NameInput, NameResolve } from './components';
import { NameInput, NameResolve } from './components';
import TabSection from 'containers/TabSection';
import { Route, Switch, RouteComponentProps } from 'react-router';
import { RouteNotFound } from 'components/RouteNotFound';
import { NewTabLink } from 'components/ui';
import { donationAddressMap } from 'config';
import translate from 'translations';
import { connect } from 'react-redux';
import { resolveDomainRequested, TResolveDomainRequested } from 'actions/ens';
import { AppState } from 'reducers';
import './index.scss';
const ENSDocsLink = () => (
<NewTabLink
@ -17,57 +12,28 @@ const ENSDocsLink = () => (
/>
);
const ENSTitle = () => (
<article className="cont-md">
<h1 className="text-center">{translate('NAV_ENS')}</h1>
<p>
The <ENSDocsLink /> is a distributed, open, and extensible naming system based on the Ethereum
blockchain. Once you have a name, you can tell your friends to send ETH to{' '}
<code>ensdomain.eth</code> instead of
<code>{donationAddressMap.ETH.substr(0, 12)}...</code>
</p>
</article>
);
interface StateProps {
ens: AppState['ens'];
}
interface DispatchProps {
resolveDomainRequested: TResolveDomainRequested;
}
type Props = StateProps & DispatchProps;
class ENSClass extends React.Component<RouteComponentProps<any> & Props> {
export default class ENSClass extends React.Component<{}> {
public render() {
const { match } = this.props;
const currentPath = match.url;
return (
<TabSection isUnavailableOffline={true}>
<section className="container">
<Switch>
<Route
exact={true}
path={currentPath}
render={() => (
<section role="main" className="row">
<ENSTitle />
<NameInput resolveDomainRequested={this.props.resolveDomainRequested} />
<NameResolve {...this.props.ens} />
<GeneralInfoPanel />
</section>
)}
/>
<RouteNotFound />
</Switch>
</section>
<div className="Tab-content">
<section className="Tab-content-pane">
<div className="ENS">
<h1 className="ENS-title">Ethereum Name Service</h1>
<p className="ENS-description">
The <ENSDocsLink /> is a distributed, open, and extensible naming system based on
the Ethereum blockchain. Once you have a name, you can tell your friends to send ETH
to <code>ensdomain.eth</code> instead of
<code>{donationAddressMap.ETH.substr(0, 12)}...</code>
</p>
<NameInput />
</div>
</section>
<NameResolve />
</div>
</TabSection>
);
}
}
const mapStateToProps = (state: AppState): StateProps => ({ ens: state.ens });
const mapDispatchToProps: DispatchProps = { resolveDomainRequested };
export default connect(mapStateToProps, mapDispatchToProps)(ENSClass);

View File

@ -47,7 +47,7 @@ function* resolveDomain(): SagaIterator {
const result: { domainData: IBaseDomainRequest; error } = yield race({
domainData: call(resolveDomainRequest, domain, node),
err: call(delay, 4000)
err: call(delay, 10000)
});
const { domainData } = result;

View File

@ -109,3 +109,7 @@ export function bytesToHuman(bytes: number) {
const i = Math.round(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.round(bytes / Math.pow(1024, i)) + ' ' + sizes[i];
}
export function ensV3Url(name: string) {
return `https://mycrypto.com/?ensname=${name}#ens`;
}

View File

@ -1,45 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`snapshot test ENS component 1`] = `
<ENSClass
history={
Object {
"action": "PUSH",
"block": [Function],
"createHref": [Function],
"go": [Function],
"goBack": [Function],
"goForward": [Function],
"length": 2,
"listen": [Function],
"location": Object {
"hash": "",
"key": "e08jz7",
"pathname": "/ens",
"search": "",
"state": Object {},
},
"push": [Function],
"replace": [Function],
}
}
location={
Object {
"hash": "",
"key": "e08jz7",
"pathname": "/ens",
"search": "",
"state": Object {},
}
}
match={
Object {
"isExact": false,
"params": Object {},
"path": "/ens",
"url": "/ens",
}
}
resolveDomainRequested={[Function]}
/>
<Connect(TabSection)
isUnavailableOffline={true}
>
<div
className="Tab-content"
>
<section
className="Tab-content-pane"
>
<div
className="ENS"
>
<h1
className="ENS-title"
>
Ethereum Name Service
</h1>
<p
className="ENS-description"
>
The
<ENSDocsLink />
is a distributed, open, and extensible naming system based on the Ethereum blockchain. Once you have a name, you can tell your friends to send ETH to
<code>
ensdomain.eth
</code>
instead of
<code>
0x4bbeEB066e
...
</code>
</p>
<Connect(NameInput) />
</div>
</section>
<Connect(NameResolve) />
</div>
</Connect(TabSection)>
`;