on master help sign in
feat: update Point view, implement Footer w/ portals, convert to as={} pattern
commit3e1a340ba55e47df161321fde040371819fe73ef authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateWed 19 Jun 2019, 17:42 (7 years ago) parentf011ffc
24 files changed+295−218
src/components/Footer.js+17 src/components/InputSigil.js+5−14 src/components/Inputs.js+1 src/components/Passport.js+25 src/components/ReactSVGComponents.js+1−2 src/components/View.js+24−4 src/components/old/advancedInput.js+5−5 src/indigo-react/components/Button.js+25−16 src/indigo-react/components/IndigoApp.js+7−1 src/indigo-react/index.js+2 src/lib/lib.js−7 src/lib/useInvites.js+34 src/lib/useSyncPoint.js+12 src/store/Provider.js+1−1 src/store/pointCache.js+29−3 src/style/bridge.scss+13 src/style/indigo-proposals.scss+6−2 src/views/InviteTicket.js+1−1 src/views/Point.js+59−140 src/views/Point/Actions.js+1−1 src/views/Point/KeysAndMetadata.js+1−1 src/views/Points.js+1−1 src/views/SentTransaction.js+1−1 src/views/ViewPoint.js+24−18
src/components/Footer.jsnew file+17
@@ -0,0 +1,17 @@
1+import React, { Children } from 'react';
2+import { createPortal } from 'react-dom';
3+
4+const kFooterPortalId = 'footer-portal';
5+
6+function Footer({ children }) {
7+ return createPortal(
8+ Children.only(children),
9+ document.getElementById(kFooterPortalId)
10+ );
11+}
12+
13+Footer.Portal = function FooterPortal(props) {
14+ return <div {...props} id={kFooterPortalId} />;
15+};
16+
17+export default Footer;
src/components/InputSigil.js+5−14
@@ -1,4 +1,5 @@
11import React from 'react';
2+import cn from 'classnames';
23import * as ob from 'urbit-ob';
34
45import Sigil from './Sigil';
@@ -31,19 +32,9 @@ export default function ValidatedSigil({
3132 const colorway = selectColorway(pass, error, focused);
3233 const valid = ob.isValidPatp(patp);
3334
34− return (
35− <div className={className}>
36− {valid ? (
37− <Sigil patp={patp} size={size} colorway={colorway} {...rest} />
38− ) : (
39− <div
40− className="bg-transparent"
41− style={{
42− width: size,
43− height: size,
44− }}
45− />
46− )}
47− </div>
35+ return valid ? (
36+ <Sigil patp={patp} size={size} colorway={colorway} margin={0} {...rest} />
37+ ) : (
38+ <div className={cn('bg-transparent', className)} />
4839 );
4940}
src/components/Inputs.js+1
@@ -78,6 +78,7 @@ export function PointInput(props) {
7878 <Input
7979 type="text"
8080 placeholder="e.g. ~zod"
81+ initialValue={props.initialValue || '~'}
8182 validators={[validatePoint, validateNotEmpty]}
8283 transformers={[prependSig]}
8384 mono
src/components/Passport.jsnew file+25
@@ -0,0 +1,25 @@
1+import React from 'react';
2+import * as ob from 'urbit-ob';
3+import { Grid, Flex } from 'indigo-react';
4+
5+import Sigil from './Sigil';
6+
7+export default function Passport({ point }) {
8+ const name = ob.patp(point);
9+ const birthDate = '2019.1.27';
10+
11+ return (
12+ <Grid gap={16} className="bg-black r8 p7">
13+ <Grid.Item cols={[1, 4]} className="bg-green">
14+ <Sigil patp={name} size={64} margin={0} />
15+ </Grid.Item>
16+ <Grid.Item as={Flex} cols={[4, 13]} col justify="between">
17+ <Flex.Item className="mono white f5">{name}</Flex.Item>
18+ <Flex.Item as={Flex} col>
19+ <Flex.Item className="mono gray4 f6 uppercase">Birth Time</Flex.Item>
20+ <Flex.Item className="mono gray4 f6">{birthDate}</Flex.Item>
21+ </Flex.Item>
22+ </Grid.Item>
23+ </Grid>
24+ );
25+}
src/components/ReactSVGComponents.js+1−2
@@ -7,8 +7,7 @@ const ReactSVGComponents = {
77 return (
88 <svg
99 {...attrs}
10− width="100%"
11− height="100%"
10+ style={{ width: '100%', height: '100%' }}
1211 version={'1.1'}
1312 xmlns={'http://www.w3.org/2000/svg'}
1413 key={key}
src/components/View.js+24−4
@@ -1,13 +1,33 @@
11import React from 'react';
22import cn from 'classnames';
3+import { Flex } from 'indigo-react';
4+
5+import Footer from './Footer';
36
47// View is a top-level component that all Views must render to inherit styling
5−function View({ className, ...rest }) {
6− return <div className={cn('ph5 mw1', className)} {...rest} />;
8+function View({ className, children, full = false, ...rest }) {
9+ return (
10+ <Flex
11+ col
12+ justify="between"
13+ className={cn(
14+ 'minh-100 ph5',
15+ {
16+ mw1: !full,
17+ 'mw2 ph9-md ph10-lg': full,
18+ },
19+ className
20+ )}
21+ {...rest}>
22+ <Flex.Item className="pv5">{children}</Flex.Item>
23+ <Footer.Portal />
24+ </Flex>
25+ );
726}
827
9−View.Full = function FullView({ className, ...rest }) {
10− return <div className={cn('mw2 ph5 ph9-md ph10-lg', className)} {...rest} />;
28+// View.Full is sugar for <View full /> to help with readability
29+View.Full = function FullView(props) {
30+ return <View full {...props} />;
1131};
1232
1333export default View;
src/components/old/advancedInput.js+5−5
@@ -1,7 +1,7 @@
11import React, { Component } from 'react';
22import PropTypes from 'prop-types';
33
4−import { compose, defaultTo, t, f } from '../../lib/lib';
4+import { compose, defaultTo } from '../../lib/lib';
55
66// =============================================================================
77// Utils
@@ -14,10 +14,10 @@ const initMessage = data => ({
1414
1515// Validation display triggers
1616const displayTriggers = {
17− eagerSuccess: r => t(r.pass),
18− eagerFailure: r => f(r.pass),
19− tactfulSuccess: r => t(r.pass) && t(r.beenFocussed),
20− tactfulFailure: r => f(r.pass) && t(r.beenFocussed) && f(r.focussed),
17+ eagerSuccess: r => r.pass,
18+ eagerFailure: r => !r.pass,
19+ tactfulSuccess: r => r.pass && r.beenFocussed,
20+ tactfulFailure: r => !r.pass && r.beenFocussed && !r.focussed,
2121};
2222
2323// =============================================================================
src/indigo-react/components/Button.js+25−16
@@ -1,23 +1,32 @@
11import React from 'react';
22import cn from 'classnames';
3+import Grid from './Grid';
34
45export default function Button({
56 solid = false,
67 disabled = false,
8+ detail,
79 className,
810 icon,
911 children,
1012 ...rest
1113}) {
14+ const textColor = {
15+ white: solid,
16+ black: !solid && !disabled,
17+ gray4: !solid && disabled,
18+ };
1219 return (
13− <a
20+ <Grid
21+ as="a"
22+ gap={4}
1423 className={cn(
15− 'pointer pv3 pl4 pr0 truncate flex-row justify-between',
24+ 'pointer p4 truncate flex-row justify-between',
1625 {
17− 'white bg-black bg-gray6-hover': solid && !disabled,
18− 'white bg-gray3': solid && disabled,
19− 'black bg-transparent bg-gray1-hover': !solid && !disabled,
20− 'gray4 bg-transparent': !solid && disabled,
26+ 'bg-black bg-gray6-hover': solid && !disabled,
27+ 'bg-gray3': solid && disabled,
28+ 'bg-transparent bg-gray1-hover': !solid && !disabled,
29+ 'bg-transparent': !solid && disabled,
2130 },
2231 className
2332 )}
@@ -25,15 +34,15 @@ export default function Button({
2534 ...(disabled && { pointerEvents: 'none', cursor: 'not-allowed' }),
2635 }}
2736 {...rest}>
28− {children}
29− <div
30− className={cn('ph4', {
31− white: solid,
32− black: !solid && !disabled,
33− gray4: !solid && disabled,
34− })}>
35− {icon}
36− </div>
37− </a>
37+ <Grid.Item className="flex-row justify-between" full>
38+ <span className={cn(textColor)}>{children}</span>
39+ <div className={cn('pl4', textColor)}>{icon}</div>
40+ </Grid.Item>
41+ {detail && (
42+ <Grid.Item full>
43+ <span className="gray4 f6">{detail}</span>
44+ </Grid.Item>
45+ )}
46+ </Grid>
3847 );
3948}
src/indigo-react/components/IndigoApp.js+7−1
@@ -2,5 +2,11 @@ import React from 'react';
22import cn from 'classnames';
33
44export default function({ debug = false, ...rest }) {
5− return <main className={cn({ debug })} {...rest} />;
5+ return (
6+ <main
7+ className={cn('minh-100', { debug })}
8+ style={{ position: 'relative' }}
9+ {...rest}
10+ />
11+ );
612}
src/indigo-react/index.js+2
@@ -1,3 +1,5 @@
11export { default as IndigoApp } from './components/IndigoApp';
2+export { default as Grid } from './components/Grid';
3+export { default as Flex } from './components/Flex';
24export { default as Input } from './components/Input';
35export { default as Button } from './components/Button';
src/lib/lib.js−7
@@ -15,11 +15,6 @@ const seq = n => Array.from(Array(n), (_, i) => i);
1515
1616const fill = (n, v) => Array.from(Array(n), () => v);
1717
18−// Terse true/false checks
19−const t = v => v === true;
20−
21−const f = v => v === false;
22−
2318const isValidGalaxy = name => {
2419 let point;
2520 try {
@@ -47,8 +42,6 @@ export {
4742 defaultTo,
4843 seq,
4944 fill,
50− t,
51− f,
5245 isValidGalaxy,
5346 randomPatq,
5447};
src/lib/useInvites.jsnew file+34
@@ -0,0 +1,34 @@
1+import { useCallback, useState } from 'react';
2+import Maybe from 'folktale/maybe';
3+import * as azimuth from 'azimuth-js';
4+
5+import { useNetwork } from 'store/network';
6+import useLifecycle from './useLifecycle';
7+
8+// fetch the invites available for a point
9+export default function useInvites(point) {
10+ const { contracts } = useNetwork();
11+
12+ const [invites, _setInvites] = useState(Maybe.Nothing());
13+
14+ const fetchInvites = useCallback(() => {
15+ const _contracts = contracts.getOrElse(null);
16+
17+ if (!contracts) {
18+ return;
19+ }
20+
21+ (async () => {
22+ const count = await azimuth.delegatedSending.getTotalUsableInvites(
23+ _contracts,
24+ point
25+ );
26+ _setInvites(Maybe.Just(count));
27+ })();
28+ }, [_setInvites, point, contracts]);
29+
30+ // fetch invites on first render
31+ useLifecycle(fetchInvites);
32+
33+ return { invites, fetchInvites };
34+}
src/lib/useSyncPoint.jsnew file+12
@@ -0,0 +1,12 @@
1+import { useEffect } from 'react';
2+
3+import { usePointCache } from 'store/pointCache';
4+
5+// sync a specified point to the cache
6+export default function useSyncPoint(point) {
7+ const { fetchPoint } = usePointCache();
8+
9+ useEffect(() => {
10+ fetchPoint(point);
11+ }, [fetchPoint, point]);
12+}
src/store/Provider.js+1−1
@@ -12,10 +12,10 @@ import { TxnCursorProvider } from './txnCursor';
1212// nest all of the providers within each other to avoid hella depth
1313export default nest([
1414 HistoryProvider,
15− TxnConfirmationsProvider,
1615 OnlineProvider,
1716 NetworkProvider,
1817 WalletProvider,
18+ TxnConfirmationsProvider,
1919 PointCursorProvider,
2020 PointCacheProvider,
2121 TxnCursorProvider,
src/store/pointCache.js+29−3
@@ -1,16 +1,42 @@
1−import React, { createContext, forwardRef, useContext, useState } from 'react';
1+import React, {
2+ createContext,
3+ forwardRef,
4+ useCallback,
5+ useContext,
6+ useState,
7+} from 'react';
8+import * as azimuth from 'azimuth-js';
9+
10+import { useNetwork } from './network';
211
312export const PointCacheContext = createContext(null);
413
514function _usePointCache() {
15+ const { contracts } = useNetwork();
616 const [pointCache, _setPointCache] = useState({});
717
8− const addToPointCache = entry =>
9− _setPointCache(cache => ({ ...cache, ...entry }));
18+ const addToPointCache = useCallback(
19+ entry => _setPointCache(cache => ({ ...cache, ...entry })),
20+ [_setPointCache]
21+ );
22+
23+ const fetchPoint = useCallback(
24+ async point => {
25+ const _contracts = contracts.getOrElse(null);
26+ if (!_contracts) {
27+ return;
28+ }
29+
30+ const details = await azimuth.azimuth.getPoint(_contracts, point);
31+ addToPointCache({ [point]: details });
32+ },
33+ [contracts, addToPointCache]
34+ );
1035
1136 return {
1237 pointCache,
1338 addToPointCache,
39+ fetchPoint,
1440 };
1541}
1642
src/style/bridge.scss+13
@@ -13,3 +13,16 @@ input:-webkit-autofill {
1313 -webkit-animation-name: autofill;
1414 -webkit-animation-fill-mode: both;
1515}
16+
17+// min-height the top elements so footer behavior works correctly
18+html {
19+ height: 100%;
20+}
21+
22+body {
23+ min-height: 100%;
24+}
25+
26+#root {
27+ min-height: 100vh;
28+}
src/style/indigo-proposals.scss+6−2
@@ -20,6 +20,10 @@
2020 background-color: #333333;
2121}
2222
23−.bg-transparent {
24− background-color: transparent;
23+.clickable {
24+ cursor: pointer;
25+}
26+
27+.minh-100 {
28+ min-height: 100vh;
2529}
src/views/InviteTicket.js+1−1
@@ -31,7 +31,7 @@ import {
3131import { generateWallet, startTransactions } from '../lib/invite';
3232import { withNetwork } from '../store/network';
3333import { withWallet } from '../store/wallet';
34−import Grid from 'components/Grid';
34+import { Grid } from 'indigo-react';
3535
3636class InviteTicket extends React.Component {
3737 constructor(props) {
src/views/Point.js+59−140
@@ -1,143 +1,62 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
3−import { pour } from 'sigil-js';
4−import * as ob from 'urbit-ob';
5−import * as azimuth from 'azimuth-js';
6−import * as need from '../lib/need';
1+import React, { useCallback } from 'react';
2+import * as need from 'lib/need';
73
8−import PointList from '../components/old/PointList';
9−import ReactSVGComponents from '../components/ReactSVGComponents';
10−import KeysAndMetadata from './Point/KeysAndMetadata';
11−import Actions from './Point/Actions';
12−import { H1, H3 } from '../components/old/Base';
13−import { withHistory } from '../store/history';
14−import { withNetwork } from '../store/network';
15−import { compose } from '../lib/lib';
16−import { withWallet } from '../store/wallet';
17−import { withPointCursor } from '../store/pointCursor';
18−import { withPointCache } from '../store/pointCache';
19−import View from 'components/View';
20−
21−class Point extends React.Component {
22− constructor(props) {
23− super(props);
24−
25− this.state = {
26− spawned: [],
27− invites: Nothing(),
28− };
29− }
30−
31− componentDidMount() {
32− this.cachePoints();
33− }
34−
35− componentDidUpdate(prevProps) {
36− //
37− // NB (jtobin)
38− //
39− // When a link in the 'issued points' component is clicked, we don't
40− // actually leave the Point component, and componentDidMount will not fire
41− // again. But, we can force a call to cachePoints here.
42− //
43− const oldCursor = prevProps.pointCursor.getOrElse(true);
44− const newCursor = this.props.pointCursor.getOrElse(true);
45−
46− if (oldCursor !== newCursor) {
47− this.cachePoints();
48− }
49− }
50−
51− cachePoints() {
52− const { web3, contracts, pointCursor, addToPointCache } = this.props;
53−
54− web3.chain(_ =>
55− contracts.chain(ctrcs =>
56− pointCursor.chain(point => {
57− azimuth.azimuth
58− .getPoint(ctrcs, point)
59− .then(details => addToPointCache({ [point]: details }));
60− azimuth.delegatedSending
61− .getTotalUsableInvites(ctrcs, point)
62− .then(count => this.setState({ invites: Just(count) }));
63− this.updateSpawned(ctrcs, point);
64− })
65− )
66− );
67− }
68−
69− updateSpawned = contracts => {
70− const { pointCursor } = this.props;
71−
72− pointCursor.matchWith({
73− Nothing: _ => {},
74− Just: point => {
75− azimuth.azimuth
76− .getSpawned(contracts, point.value)
77− .then(spawned => this.setState({ spawned }));
78− },
79− });
80− };
81−
82− render() {
83− const { web3, history, wallet, pointCache } = this.props;
84−
85− const { spawned } = this.state;
4+import { usePointCursor } from 'store/pointCursor';
5+// import { usePointCache } from 'store/pointCache';
866
87− const point = need.pointCursor(this.props.pointCursor);
88−
89− const pointDetails =
90− point in pointCache ? Just(pointCache[point]) : Nothing();
91−
92− const name = ob.patp(point);
93−
94− const sigil = pour({
95− patp: name,
96− renderer: ReactSVGComponents,
97− size: 256,
98− });
99−
100− const online = Just.hasInstance(web3);
101−
102− const authenticated = Just.hasInstance(wallet);
103−
104− const issuedPointList =
105− spawned.length === 0 ? (
106− <div />
107− ) : (
108− <div>
109− <H3>{'Issued Points'}</H3>
110−
111− <PointList routeHandler={history.popAndPush} points={spawned} />
112− </div>
113− );
114−
115− return (
116− <View>
117− <div className={'mt-12 pt-6'}>{sigil}</div>
118− <H1>
119− <code>{name}</code>
120− </H1>
121− {authenticated ? (
122− <Actions
123− online={online}
124− point={point}
125− pointDetails={pointDetails}
126− invites={this.state.invites}
127− />
128− ) : null}
129−
130− {online ? <KeysAndMetadata pointDetails={pointDetails} /> : <div />}
131− {issuedPointList}
132− </View>
133− );
134− }
7+import View from 'components/View';
8+import useInvites from 'lib/useInvites';
9+import useSyncPoint from 'lib/useSyncPoint';
10+import Passport from 'components/Passport';
11+import { Grid } from 'indigo-react';
12+import { ForwardButton } from 'components/Buttons';
13+import { useHistory } from 'store/history';
14+import { ROUTE_NAMES } from 'lib/routeNames';
15+import Footer from 'components/Footer';
16+
17+export default function Point() {
18+ const history = useHistory();
19+ const { pointCursor } = usePointCursor();
20+ // const { pointCache } = usePointCache();
21+
22+ const point = need.pointCursor(pointCursor);
23+
24+ // fetch the invites for the current cursor
25+ const { invites } = useInvites(point);
26+
27+ const goAdmin = useCallback(() => {}, []);
28+ // ^ TODO: push .ADMIN
29+ const goInvite = useCallback(() => history.push(ROUTE_NAMES.INVITES_MANAGE), [
30+ history,
31+ ]);
32+
33+ // sync the current cursor
34+ useSyncPoint(point);
35+
36+ // const pointDetails = need.fromPointCache(pointCache, point);
37+
38+ return (
39+ <View>
40+ <Passport point={point} />
41+ <Grid className="pt2">
42+ <Grid.Item full>
43+ <ForwardButton onClick={goAdmin}>Admin</ForwardButton>
44+ </Grid.Item>
45+ <Grid.Divider />
46+ <Grid.Item full>
47+ <ForwardButton detail="Boot your computer" disabled>
48+ Boot Arvo
49+ </ForwardButton>
50+ </Grid.Item>
51+ </Grid>
52+ <Footer>
53+ <Grid className="pt2">
54+ <Grid.Divider />
55+ <Grid.Item full>
56+ <ForwardButton onClick={goInvite}>Invite</ForwardButton>
57+ </Grid.Item>
58+ </Grid>
59+ </Footer>
60+ </View>
61+ );
13562}
136−
137−export default compose(
138− withNetwork,
139− withHistory,
140− withWallet,
141− withPointCursor,
142− withPointCache
143−)(Point);
src/views/Point/Actions.js+1−1
@@ -8,7 +8,7 @@ import { Button } from '../../components/old/Base';
88import { ROUTE_NAMES } from '../../lib/routeNames';
99import { useHistory } from '../../store/history';
1010import { useWallet } from '../../store/wallet';
11−import Grid from 'components/Grid';
11+import { Grid } from 'indigo-react';
1212
1313const isPlanet = point =>
1414 azimuth.getPointSize(point) === azimuth.PointSize.Planet;
src/views/Point/KeysAndMetadata.js+1−1
@@ -2,7 +2,7 @@ import React from 'react';
22import { H2, H4 } from '../../components/old/Base';
33
44import { ETH_ZERO_ADDR, eqAddr } from '../../lib/wallet';
5−import Grid from 'components/Grid';
5+import { Grid } from 'indigo-react';
66
77const NULL_KEY =
88 '0x0000000000000000000000000000000000000000000000000000000000000000';
src/views/Points.js+1−1
@@ -13,7 +13,7 @@ import { compose } from '../lib/lib';
1313import { withWallet } from '../store/wallet';
1414import { withPointCache } from '../store/pointCache';
1515import View from 'components/View';
16−import Grid from 'components/Grid';
16+import { Grid } from 'indigo-react';
1717
1818const hasTransferProxy = (cache, point) =>
1919 point in cache ? !eqAddr(cache[point].transferProxy, ETH_ZERO_ADDR) : false;
src/views/SentTransaction.js+1−1
@@ -12,7 +12,7 @@ import { useTxnConfirmations } from '../store/txnConfirmations';
1212import { useNetwork } from '../store/network';
1313import { useTxnCursor } from '../store/txnCursor';
1414import View from 'components/View';
15−import Grid from 'components/Grid';
15+import { Grid } from 'indigo-react';
1616
1717class Success extends React.Component {
1818 constructor(props) {
src/views/ViewPoint.js+24−18
@@ -1,4 +1,4 @@
1−import React, { useState } from 'react';
1+import React, { useCallback, useState } from 'react';
22import Maybe from 'folktale/maybe';
33import * as ob from 'urbit-ob';
44
@@ -22,11 +22,18 @@ export default function ViewPoint() {
2222 const [error, setError] = useState();
2323 const [focused, setFocused] = useState(true);
2424
25+ const disabled = !pass;
26+ const goForward = useCallback(() => {
27+ setPointCursor(Maybe.Just(parseInt(ob.patp2dec(pointName), 10)));
28+ // ^ pointCursor expects native number type, not string
29+ history.popAndPush(ROUTE_NAMES.POINT);
30+ }, [setPointCursor, history, pointName]);
31+
2532 return (
2633 <View>
27− <H1>{'View a Point'}</H1>
34+ <H1>View a Point</H1>
2835
29− <P>{'Enter a point name to view its public information.'}</P>
36+ <P>Enter a point name to view its public information.</P>
3037
3138 <PointInput
3239 name="point"
@@ -35,25 +42,24 @@ export default function ViewPoint() {
3542 onPass={setPass}
3643 onError={setError}
3744 onFocus={setFocused}
45+ onEnter={goForward}
46+ accessory={
47+ <InputSigil
48+ patp={pointName}
49+ size={68}
50+ margin={8}
51+ pass={pass}
52+ focused={focused}
53+ error={error}
54+ />
55+ }
3856 autoFocus
3957 />
4058
41− <InputSigil
42− patp={pointName}
43− size={68}
44− margin={8}
45− pass={pass}
46− focused={focused}
47− error={error}
48− />
49−
5059 <ForwardButton
51− disabled={!pass}
52− onClick={() => {
53− setPointCursor(Maybe.Just(parseInt(ob.patp2dec(pointName), 10)));
54− // ^ pointCursor expects native number type, not string
55− history.popAndPush(ROUTE_NAMES.POINT);
56− }}
60+ className="mt3"
61+ disabled={disabled}
62+ onClick={goForward}
5763 solid>
5864 Continue
5965 </ForwardButton>