on master help sign in
feat: allow nested local routers and build out Invite/URL
commit6542dce88b005bcf884a21009f536737fbb61016 authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateWed 19 Jun 2019, 19:57 (7 years ago) parenta8044e9
18 files changed+387−120
src/Bridge.js+8−5 src/components/Crumbs.js+32 src/components/FooterButton.js+17 src/components/Router.js+1−5 src/indigo-react/components/Button.js+11−5 src/indigo-react/components/Typography.js+23 src/indigo-react/index.js+2 src/lib/routeNames.js+1−1 src/lib/router.js+4−7 src/lib/useCurrentPointName.js+12 src/lib/useInvites.js+10−4 src/lib/useRouter.js+102 src/store/history.js+12−59 src/views/Invite.js+123 src/views/Landing.js+13−12 src/views/Mnemonic.js+3−1 src/views/Point.js+12−20 src/views/Point/Actions.js+1−1
src/Bridge.js+8−5
@@ -7,6 +7,7 @@ import Router from 'components/Router';
77import Provider from 'store/Provider';
88
99import { ROUTE_NAMES } from 'lib/routeNames';
10+import { ROUTES } from 'lib/router';
1011import { NETWORK_TYPES } from 'lib/network';
1112import { walletFromMnemonic } from 'lib/wallet';
1213import { isDevelopment } from 'lib/flags';
@@ -22,12 +23,12 @@ const shouldStubLocal = process.env.REACT_APP_STUB_LOCAL === 'true';
2223const kIsStubbed = isDevelopment && shouldStubLocal;
2324const kInitialRoutes = kIsStubbed
2425 ? [
25− { name: ROUTE_NAMES.LANDING },
26− { name: ROUTE_NAMES.NETWORK },
27− { name: ROUTE_NAMES.WALLET },
28− { name: ROUTE_NAMES.POINTS },
26+ { key: ROUTE_NAMES.LANDING },
27+ { key: ROUTE_NAMES.NETWORK },
28+ { key: ROUTE_NAMES.WALLET },
29+ { key: ROUTE_NAMES.POINTS },
2930 ]
30− : [{ name: ROUTE_NAMES.LANDING }];
31+ : [{ key: ROUTE_NAMES.LANDING }];
3132
3233const kInitialWallet = kIsStubbed
3334 ? walletFromMnemonic(
@@ -43,6 +44,8 @@ const kInitialPointCursor = kIsStubbed ? Just(0) : Nothing();
4344export default function Bridge() {
4445 return (
4546 <Provider
47+ views={ROUTES}
48+ names={ROUTE_NAMES}
4649 initialRoutes={kInitialRoutes}
4750 initialNetworkType={kInitialNetworkType}
4851 initialWallet={kInitialWallet}
src/components/Crumbs.jsnew file+32
@@ -0,0 +1,32 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { Flex, Breadcrumb } from 'indigo-react';
4+
5+export default function Crumbs({ className, routes = [] }) {
6+ const lastIndex = routes.length - 1;
7+ const textStyle = 'gray4 mono';
8+ return (
9+ <Flex className={className} row wrap>
10+ {routes.map((route, i) => {
11+ const disabled = !route.action;
12+
13+ return (
14+ <React.Fragment key={route.text}>
15+ <Flex.Item
16+ as={Breadcrumb}
17+ onClick={route.action}
18+ disabled={disabled}
19+ className={cn(textStyle, { 'clickable underline': !disabled })}>
20+ {route.text}
21+ </Flex.Item>
22+ {i !== lastIndex && (
23+ <Flex.Item as={Breadcrumb} className={cn(textStyle, 'mh4')}>
24+ /
25+ </Flex.Item>
26+ )}
27+ </React.Fragment>
28+ );
29+ })}
30+ </Flex>
31+ );
32+}
src/components/FooterButton.jsnew file+17
@@ -0,0 +1,17 @@
1+import React from 'react';
2+import { Grid, Button } from 'indigo-react';
3+
4+import Footer from './Footer';
5+
6+export default function FooterButton(props) {
7+ return (
8+ <Footer>
9+ <Grid className="pt2">
10+ <Grid.Divider />
11+ <Grid.Item full>
12+ <Button {...props} />
13+ </Grid.Item>
14+ </Grid>
15+ </Footer>
16+ );
17+}
src/components/Router.js+1−5
@@ -1,13 +1,9 @@
11import React from 'react';
22
33import { useHistory } from 'store/history';
4−import router from 'lib/router';
54
6−// router is just a component that renders a specific view
7−// depending on the lastest route in the history
85export default function Router() {
9− const history = useHistory();
10− const Route = router(history.peek());
6+ const { Route } = useHistory();
117
128 return <Route />;
139}
src/indigo-react/components/Button.js+11−5
@@ -1,13 +1,16 @@
11import React from 'react';
22import cn from 'classnames';
3+
34import Grid from './Grid';
5+import Flex from './Flex';
6+import { HelpText } from './Typography';
47
58export default function Button({
69 solid = false,
710 disabled = false,
811 detail,
912 className,
10− icon,
13+ icon = '→',
1114 children,
1215 ...rest
1316}) {
@@ -31,16 +34,19 @@ export default function Button({
3134 className
3235 )}
3336 style={{
34− ...(disabled && { pointerEvents: 'none', cursor: 'not-allowed' }),
37+ ...(disabled && {
38+ pointerEvents: 'none',
39+ cursor: 'not-allowed',
40+ }),
3541 }}
3642 {...rest}>
37− <Grid.Item className="flex-row justify-between" full>
43+ <Grid.Item as={Flex} justify="between" full>
3844 <span className={cn(textColor)}>{children}</span>
3945 <div className={cn('pl4', textColor)}>{icon}</div>
4046 </Grid.Item>
4147 {detail && (
42− <Grid.Item full>
43− <span className="gray4 f6">{detail}</span>
48+ <Grid.Item as={HelpText} full>
49+ {detail}
4450 </Grid.Item>
4551 )}
4652 </Grid>
src/indigo-react/components/Typography.jsnew file+23
@@ -0,0 +1,23 @@
1+import React from 'react';
2+import cn from 'classnames';
3+
4+import getComponent from '../lib/getComponent';
5+
6+const typeWith = (DefaultComponent, defaultClassName) =>
7+ function({ as = DefaultComponent, className, ...rest }) {
8+ const Component = getComponent(as);
9+ return <Component className={cn(defaultClassName, className)} {...rest} />;
10+ };
11+
12+export const H1 = typeWith('h1');
13+export const H2 = typeWith('h2');
14+export const H3 = typeWith('h3');
15+export const H4 = typeWith('h4');
16+export const H5 = typeWith('h5');
17+export const H6 = typeWith('h6');
18+export const Caption = typeWith('caption');
19+export const P = typeWith('p');
20+
21+export const Breadcrumb = typeWith('span', 'f6');
22+export const Text = typeWith('span', 'f5');
23+export const HelpText = typeWith('span', 'f6 gray4');
src/indigo-react/index.js+2
@@ -3,3 +3,5 @@ export { default as Grid } from './components/Grid';
33export { default as Flex } from './components/Flex';
44export { default as Input } from './components/Input';
55export { default as Button } from './components/Button';
6+
7+export * from './components/Typography';
src/lib/routeNames.js+1−1
@@ -3,7 +3,7 @@ export const ROUTE_NAMES = {
33 //
44 INVITE_TICKET: Symbol('INVITE_TICKET'),
55 INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
6− INVITES_SEND: Symbol('INVITES_SEND'),
6+ INVITE: Symbol('INVITE'),
77 INVITES_MANAGE: Symbol('INVITES_MANAGE'),
88 //
99 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
src/lib/router.js+4−7
@@ -4,7 +4,6 @@ import * as need from './need';
44
55import Landing from '../views/Landing';
66import InviteTicket from '../views/InviteTicket';
7−import InvitesSend from '../views/InvitesSend.js';
87import InvitesManage from '../views/InvitesManage.js';
98import AcceptTransfer from '../views/AcceptTransfer';
109import CancelTransfer from '../views/CancelTransfer';
@@ -31,16 +30,17 @@ import Shards from '../views/Shards';
3130import Ticket from '../views/Ticket';
3231import Transfer from '../views/Transfer';
3332import Wallet from '../views/Wallet';
33+import Invite from 'views/Invite';
3434
3535import { EthereumWallet } from './wallet';
3636import { renderNetworkType } from './network';
3737import { ROUTE_NAMES } from './routeNames';
3838
39−const ROUTES = {
39+export const ROUTES = {
4040 [ROUTE_NAMES.DEFAULT]: Landing,
4141 [ROUTE_NAMES.LANDING]: Landing,
4242 [ROUTE_NAMES.INVITE_TICKET]: InviteTicket,
43− [ROUTE_NAMES.INVITES_SEND]: InvitesSend,
43+ [ROUTE_NAMES.INVITE]: Invite,
4444 [ROUTE_NAMES.INVITES_MANAGE]: InvitesManage,
4545 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
4646 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
@@ -77,7 +77,7 @@ export const getRouteBreadcrumb = ({
7777 return 'Invite code';
7878 case ROUTE_NAMES.INVITE_TRANSACTIONS:
7979 return 'Setting up new wallet';
80− case ROUTE_NAMES.INVITES_SEND:
80+ case ROUTE_NAMES.INVITE:
8181 return 'Send invites';
8282 case ROUTE_NAMES.INVITES_MANAGE:
8383 return 'Manage invites';
@@ -138,6 +138,3 @@ export const getRouteBreadcrumb = ({
138138 return 'Bridge';
139139 }
140140};
141−
142−const router = route => ROUTES[route.name];
143−export default router;
src/lib/useCurrentPointName.jsnew file+12
@@ -0,0 +1,12 @@
1+import * as ob from 'urbit-ob';
2+
3+import { usePointCursor } from 'store/pointCursor';
4+
5+export default function useCurrentPointName() {
6+ const { pointCursor } = usePointCursor();
7+
8+ return pointCursor.matchWith({
9+ Nothing: () => null,
10+ Just: p => ob.patp(p.value),
11+ });
12+}
src/lib/useInvites.js+10−4
@@ -9,7 +9,7 @@ import useLifecycle from './useLifecycle';
99export default function useInvites(point) {
1010 const { contracts } = useNetwork();
1111
12− const [invites, _setInvites] = useState(Maybe.Nothing());
12+ const [availableInvites, _setAvailableInvites] = useState(Maybe.Nothing());
1313
1414 const fetchInvites = useCallback(() => {
1515 const _contracts = contracts.getOrElse(null);
@@ -23,12 +23,18 @@ export default function useInvites(point) {
2323 _contracts,
2424 point
2525 );
26− _setInvites(Maybe.Just(count));
26+ _setAvailableInvites(Maybe.Just(count));
2727 })();
28− }, [_setInvites, point, contracts]);
28+ }, [_setAvailableInvites, point, contracts]);
2929
3030 // fetch invites on first render
3131 useLifecycle(fetchInvites);
3232
33− return { invites, fetchInvites };
33+ return {
34+ availableInvites,
35+ fetchInvites,
36+ // TODO: look up sent/accepted
37+ sentInvites: Maybe.Just(6),
38+ acceptedInvites: Maybe.Just(5),
39+ };
3440}
src/lib/useRouter.jsnew file+102
@@ -0,0 +1,102 @@
1+import { useCallback, useMemo, useState, useEffect } from 'react';
2+import { last, includes as _includes } from 'lodash';
3+
4+/**
5+ * @param primary whether or not this is the top-level router
6+ * @param names map of string to view key, used for consumer references
7+ * @param views map of view keys to react component views
8+ * @param initialRoutes initial set of routes following the { key, data } format
9+ */
10+export default function useRouter({
11+ primary = false,
12+ names = {},
13+ views = {},
14+ initialRoutes = [],
15+}) {
16+ const [routes, setRoutes] = useState(initialRoutes);
17+
18+ const push = useCallback(
19+ (key, data) => {
20+ if (!views[key]) {
21+ throw new Error(
22+ `Invalid Route: ${key}. Possible Routes: ${Object.keys(views).join(
23+ ', '
24+ )}`
25+ );
26+ }
27+
28+ setRoutes(routes => [...routes, { key, data }]);
29+ },
30+ [setRoutes, views]
31+ );
32+ const popAndPush = useCallback(
33+ (key, data = {}) => {
34+ if (!views[key]) {
35+ throw new Error(
36+ `Invalid Route: ${key}. Possible Routes: ${Object.keys(views).join(
37+ ', '
38+ )}`
39+ );
40+ }
41+
42+ setRoutes(routes => [
43+ ...routes.slice(0, routes.length - 1),
44+ { key, data },
45+ ]);
46+ },
47+ [setRoutes, views]
48+ );
49+ const pop = useCallback(
50+ (count = 1) =>
51+ setRoutes(routes =>
52+ routes.length > 1 ? [...routes.slice(0, routes.length - count)] : routes
53+ ),
54+ // ^ can't pop root route
55+ [setRoutes]
56+ );
57+ const peek = useCallback(() => last(routes), [routes]);
58+ const includes = useCallback(key => _includes(routes.map(r => r.key), key), [
59+ routes,
60+ ]);
61+ const size = routes.length;
62+ const data = useMemo(() => last(routes).data, [routes]);
63+ const Route = useMemo(() => views[peek().key], [views, peek]);
64+
65+ // Scroll to top of page with each route transition on primary navigation
66+ useEffect(() => {
67+ if (!primary) {
68+ return;
69+ }
70+
71+ window.scrollTo(0, 0);
72+ }, [routes, primary]);
73+
74+ // capture browser pop in primary router
75+ useEffect(() => {
76+ if (!primary) {
77+ return;
78+ }
79+
80+ window.history.pushState(null, null, null);
81+
82+ window.onpopstate = e => {
83+ window.history.pushState(null, null, null);
84+ pop();
85+ };
86+
87+ // TODO: disposer functon
88+ }, [pop, primary]);
89+
90+ return {
91+ Route,
92+ names,
93+ routes,
94+ push,
95+ popAndPush,
96+ pop,
97+ peek,
98+ size,
99+ includes,
100+ data,
101+ };
102+}
src/store/history.js+12−59
@@ -1,67 +1,20 @@
1−import React, {
2− createContext,
3− forwardRef,
4− useContext,
5− useState,
6− useEffect,
7−} from 'react';
8−import { last, includes } from 'lodash';
1+import React, { createContext, forwardRef, useContext } from 'react';
92
10−import { BRIDGE_ERROR } from '../lib/error';
11−import { ROUTE_NAMES } from '../lib/routeNames';
3+import useRouter from 'lib/useRouter';
124
13−export const HistoryContext = createContext(null);
14−
15−function _useHistory(initialRoutes = []) {
16− const [history, setHistory] = useState(initialRoutes);
17−
18− const value = {
19− routes: history,
20− push: (name, data) => {
21− if (!includes(ROUTE_NAMES, name)) {
22− throw BRIDGE_ERROR.INVALID_ROUTE;
23− }
24−
25− setHistory(history => [...history, { name, data }]);
26− },
27− popAndPush: (name, data = {}) =>
28− setHistory(history => [
29− ...history.slice(0, history.length - 1),
30− { name, data },
31− ]),
32− pop: (count = 1) =>
33− setHistory(history => [...history.slice(0, history.length - count)]),
34− peek: () => last(history),
35− size: history.length,
36− includes: name => includes(history.map(r => r.name), name),
37− data: last(history).data,
38− };
39−
40− // Scroll to top of page with each route transition.
41− useEffect(() => window.scrollTo(0, 0), [history]);
5+/**
6+ * `useHistory` is just a global `useRouter`
7+ * and we call it `history` to distinguish it within a local router.
8+ * (see Invite.js for an example)
9+ */
4210
43− // set up handlers
44− useEffect(() => {
45− window.history.pushState(null, null, null);
46−
47− window.onpopstate = e => {
48− window.history.pushState(null, null, null);
49− value.pop();
50− };
51−
52− // TODO: disposer functon
53− });
54−
55− return value;
56−}
11+export const HistoryContext = createContext(null);
5712
58−export function HistoryProvider({ initialRoutes, children }) {
59− const history = _useHistory(initialRoutes);
13+export function HistoryProvider({ names, views, initialRoutes, children }) {
14+ const router = useRouter({ names, views, initialRoutes, primary: true });
6015
6116 return (
62− <HistoryContext.Provider value={history}>
63− {children}
64− </HistoryContext.Provider>
17+ <HistoryContext.Provider value={router}>{children}</HistoryContext.Provider>
6518 );
6619}
6720
@@ -74,6 +27,6 @@ export function useHistory() {
7427export const withHistory = Component =>
7528 forwardRef((props, ref) => (
7629 <HistoryContext.Consumer>
77− {history => <Component ref={ref} history={history} {...props} />}
30+ {router => <Component ref={ref} history={router} {...props} />}
7831 </HistoryContext.Consumer>
7932 ));
src/views/Invite.jsnew file+123
@@ -0,0 +1,123 @@
1+import React, { useCallback } from 'react';
2+import * as need from 'lib/need';
3+import { Grid, H5, Text, Button } from 'indigo-react';
4+
5+import { usePointCursor } from 'store/pointCursor';
6+// import { usePointCache } from 'store/pointCache';
7+
8+import View from 'components/View';
9+import Crumbs from 'components/Crumbs';
10+
11+import useInvites from 'lib/useInvites';
12+import useSyncPoint from 'lib/useSyncPoint';
13+import useCurrentPointName from 'lib/useCurrentPointName';
14+
15+import { useHistory } from 'store/history';
16+import { ForwardButton } from 'components/Buttons';
17+import FooterButton from 'components/FooterButton';
18+import useRouter from 'lib/useRouter';
19+import InvitesSend from './InvitesSend';
20+
21+const kInviteNames = {
22+ URL: 'URL',
23+ EMAIL: 'EMAIL',
24+};
25+
26+const kInviteViews = {
27+ URL: InviteUrl,
28+ EMAIL: InvitesSend,
29+};
30+
31+const valOrLoading = val =>
32+ val.matchWith({
33+ Nothing: () => '▓',
34+ Just: p => p.value,
35+ });
36+
37+function Highlighted(props) {
38+ return <span className="red3" {...props} />;
39+}
40+
41+function InviteUrl({ names, push }) {
42+ const pushEmail = useCallback(() => push(names.EMAIL), [push, names]);
43+
44+ return (
45+ <>
46+ <Grid className="mt3">
47+ <Grid.Divider />
48+ <Grid.Item
49+ as={Button}
50+ detail="Generate a URL to send to one guest"
51+ full
52+ disabled>
53+ Generate Invite
54+ </Grid.Item>
55+ <Grid.Divider />
56+ <Grid.Item
57+ as={ForwardButton}
58+ onClick={pushEmail}
59+ detail="Send invitation to your guest's email"
60+ full>
61+ Send Invite via Email
62+ </Grid.Item>
63+ </Grid>
64+ <FooterButton detail="i.e. Ethereum Address, Text File" disabled>
65+ Advanced
66+ </FooterButton>
67+ </>
68+ );
69+}
70+
71+export default function Invite() {
72+ const history = useHistory();
73+ const { Route, ...routeProps } = useRouter({
74+ names: kInviteNames,
75+ views: kInviteViews,
76+ initialRoutes: [{ key: kInviteNames.URL }],
77+ });
78+ const { pointCursor } = usePointCursor();
79+ const point = need.pointCursor(pointCursor);
80+ const name = useCurrentPointName();
81+
82+ // sync the current cursor
83+ useSyncPoint(point);
84+
85+ // fetch the invites for the current cursor
86+ const { availableInvites, sentInvites, acceptedInvites } = useInvites(point);
87+ const availableInvitesText = valOrLoading(availableInvites);
88+ const sentInvitesText = valOrLoading(sentInvites);
89+ const acceptedInvitesText = valOrLoading(acceptedInvites);
90+
91+ return (
92+ <View>
93+ <Grid className="mb3">
94+ <Grid.Item
95+ as={Crumbs}
96+ routes={[
97+ {
98+ text: name,
99+ action: () => history.pop(),
100+ },
101+ {
102+ text: 'Invite',
103+ },
104+ ]}
105+ full
106+ />
107+ <Grid.Item as={H5} className="mv4" full>
108+ Invite
109+ </Grid.Item>
110+ <Grid.Item as={Text} full>
111+ You currently have{' '}
112+ <Highlighted>{availableInvitesText} invitations</Highlighted> left.
113+ <br />
114+ Out of the <Highlighted>{sentInvitesText}</Highlighted> invites you
115+ sent, <Highlighted>{acceptedInvitesText}</Highlighted> have been
116+ accepted.
117+ </Grid.Item>
118+ </Grid>
119+ {/* TODO(shrugs): use context isntead of this routeProps nonsense */}
120+ <Route {...routeProps} />
121+ </View>
122+ );
123+}
src/views/Landing.js+13−12
@@ -1,5 +1,5 @@
11import React from 'react';
2−import { H1, P } from '../components/old/Base';
2+import { H1, P, HelpText } from 'indigo-react';
33
44import { ROUTE_NAMES } from '../lib/routeNames';
55import { useHistory } from '../store/history';
@@ -19,25 +19,26 @@ function Landing(props) {
1919 address space.
2020 </P>
2121
22− <ForwardButton onClick={() => history.push(ROUTE_NAMES.INVITE_TICKET)}>
22+ <ForwardButton
23+ className="mt3"
24+ detail="If you were sent an Azimuth invite code."
25+ onClick={() => history.push(ROUTE_NAMES.INVITE_TICKET)}>
2326 Claim an invite
2427 </ForwardButton>
2528
26− <P>If you were sent an Azimuth invite code, start here.</P>
27−
28− <ForwardButton onClick={() => history.push(ROUTE_NAMES.NETWORK)}>
29+ <ForwardButton
30+ className="mt3"
31+ detail="If you own Azimuth assets and want to manage them in some way."
32+ onClick={() => history.push(ROUTE_NAMES.NETWORK)}>
2933 Unlock a Wallet
3034 </ForwardButton>
3135
32− <P>
33− If you own Azimuth assets and want to manage them in some way, start
34− here. You'll need either your Urbit ticket or a keypair.
35− </P>
36−
37− <ForwardButton onClick={() => history.push(ROUTE_NAMES.VIEW_POINT)}>
36+ <ForwardButton
37+ className="mt3"
38+ detail="View an Azimuth point without signing into a wallet."
39+ onClick={() => history.push(ROUTE_NAMES.VIEW_POINT)}>
3840 View a point
3941 </ForwardButton>
40− <P>{'View an Azimuth point without signing into a wallet.'}</P>
4142 </View>
4243 );
4344}
src/views/Mnemonic.js+3−1
@@ -65,8 +65,10 @@ export default function Mnemonic() {
6565 />
6666
6767 <ForwardButton
68+ className="mt3"
6869 disabled={Nothing.hasInstance(wallet)}
69− onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}>
70+ onClick={() => history.popAndPush(ROUTE_NAMES.POINTS)}
71+ solid>
7072 Continue
7173 </ForwardButton>
7274 </View>
src/views/Point.js+12−20
@@ -8,13 +8,13 @@ import { usePointCursor } from 'store/pointCursor';
88import View from 'components/View';
99import Passport from 'components/Passport';
1010import { ForwardButton } from 'components/Buttons';
11−import Footer from 'components/Footer';
1211
1312import useInvites from 'lib/useInvites';
1413import useSyncPoint from 'lib/useSyncPoint';
1514import { ROUTE_NAMES } from 'lib/routeNames';
1615
1716import { useHistory } from 'store/history';
17+import FooterButton from 'components/FooterButton';
1818
1919export default function Point() {
2020 const history = useHistory();
@@ -24,11 +24,9 @@ export default function Point() {
2424 const point = need.pointCursor(pointCursor);
2525
2626 // fetch the invites for the current cursor
27− const { invites } = useInvites(point);
27+ const { availableInvites } = useInvites(point);
2828
29− const goAdmin = useCallback(() => {}, []);
30− // ^ TODO: push .ADMIN
31− const goInvite = useCallback(() => history.push(ROUTE_NAMES.INVITES_MANAGE), [
29+ const goInvite = useCallback(() => history.push(ROUTE_NAMES.INVITE), [
3230 history,
3331 ]);
3432
@@ -42,7 +40,7 @@ export default function Point() {
4240 <Passport point={point} />
4341 <Grid className="pt2">
4442 <Grid.Item full>
45− <ForwardButton onClick={goAdmin}>Admin</ForwardButton>
43+ <ForwardButton disabled>Admin</ForwardButton>
4644 </Grid.Item>
4745 <Grid.Divider />
4846 <Grid.Item full>
@@ -51,20 +49,14 @@ export default function Point() {
5149 </ForwardButton>
5250 </Grid.Item>
5351 </Grid>
54− <Footer>
55− <Grid className="pt2">
56− <Grid.Divider />
57− <Grid.Item full>
58− <ForwardButton onClick={goInvite}>
59− Invite
60− {invites.matchWith({
61− Nothing: () => null,
62− Just: p => <sup className="ml1">{p.value}</sup>,
63− })}
64− </ForwardButton>
65− </Grid.Item>
66− </Grid>
67− </Footer>
52+
53+ <FooterButton onClick={goInvite}>
54+ Invite
55+ {availableInvites.matchWith({
56+ Nothing: () => null,
57+ Just: p => <sup className="ml1">{p.value}</sup>,
58+ })}
59+ </FooterButton>
6860 </View>
6961 );
7062}
src/views/Point/Actions.js+1−1
@@ -111,7 +111,7 @@ function Actions(props) {
111111 disabled={!isActiveOwner || !online || !hasInvites}
112112 prop-size={'sm'}
113113 prop-type={'link'}
114− onClick={() => history.push(ROUTE_NAMES.INVITES_SEND)}>
114+ onClick={() => history.push(ROUTE_NAMES.INVITE)}>
115115 {'Send invites ('}
116116 {invites.matchWith({
117117 Nothing: () => '?',