on master help sign in
Merge pull request #209 from urbit/feat/design-pass
261ea40Fang
src/components/Accordion.jsnew file+47
@@ -0,0 +1,47 @@
1+import React from 'react';
2+import { Grid, AccessoryIcon } from 'indigo-react';
3+
4+export default function Accordion({
5+ views,
6+ options,
7+ currentTab,
8+ onTabChange,
9+ className,
10+ ...rest
11+}) {
12+ const Tab = views[currentTab];
13+
14+ return (
15+ <Grid className={className}>
16+ {options.map((option, i) => {
17+ const isActive = option.value === currentTab;
18+
19+ return (
20+ <React.Fragment key={option.value}>
21+ <Grid.Item
22+ full
23+ className="f5 pv3 rel clickable"
24+ onClick={() => onTabChange(isActive ? undefined : option.value)}>
25+ {option.text}
26+ <div
27+ className="abs"
28+ style={{
29+ top: 0,
30+ right: 0,
31+ height: '100%',
32+ width: '44px',
33+ overflow: 'hidden',
34+ }}>
35+ <AccessoryIcon className="black">
36+ {isActive ? '▲' : '▼'}
37+ </AccessoryIcon>
38+ </div>
39+ </Grid.Item>
40+ {isActive && <Grid.Item full as={Tab} />}
41+ <Grid.Divider />
42+ </React.Fragment>
43+ );
44+ })}
45+ </Grid>
46+ );
47+}
src/components/Tabs.js+17−6
@@ -15,23 +15,34 @@ export default function Tabs({
15 return (
15 return (
16 <Grid className={className}>
16 <Grid className={className}>
17 <Grid.Item full as={Flex} wrap className="b-gray3 bb1">
17 <Grid.Item full as={Flex} wrap className="b-gray3 bb1">
18− {options.map(option => {
18+ {options.map((option, i) => {
19 const isActive = option.value === currentTab;
19 const isActive = option.value === currentTab;
20+ const isFirst = i === 0;
21+
20 return (
22 return (
21 <Flex.Item
23 <Flex.Item
22 key={option.value}
24 key={option.value}
23 onClick={() => onTabChange(option.value)}
25 onClick={() => onTabChange(option.value)}
26+ className={cn(
24− className={cn('f5 pv3 mr6 clickable', {
27+ 'f5 pv3 clickable',
28+ {
25− 'black b-black bb1': isActive,
29+ 'black b-black bb1': isActive,
26− gray3: !isActive,
30+ gray3: !isActive,
31+ },
32+ {
33+ // all items have right margin/padding
34+ 'mr2 pr2': true,
35+ // the first one is flush to the left
36+ 'ml2 pl2': !isFirst,
37+ }
27− })}
38+ )}
28 style={{ marginBottom: '-1px' }}>
39 style={{ marginBottom: '-1px' }}>
29 {option.text}
40 {option.text}
30 </Flex.Item>
41 </Flex.Item>
31 );
42 );
32 })}
43 })}
33 </Grid.Item>
44 </Grid.Item>
34− <Grid.Item full as={Tab} className="mt6" {...rest} />
45+ <Grid.Item full as={Tab} {...rest} />
35 </Grid>
46 </Grid>
36 );
47 );
37}
48}
src/indigo-react/components/CheckboxInput.js+1
@@ -23,6 +23,7 @@ export default function CheckboxInput({
23 bind,
23 bind,
24 autoFocus,
24 autoFocus,
25 disabled,
25 disabled,
26+ touched,
26
27
27 // ignored
28 // ignored
28 initialValue,
29 initialValue,
src/indigo-react/components/Input.js+9−3
@@ -6,6 +6,7 @@ import { ErrorText } from './Typography';
6
6
7export default React.memo(function Input({
7export default React.memo(function Input({
8 // visuals
8 // visuals
9+ type,
9 name,
10 name,
10 label,
11 label,
11 className,
12 className,
@@ -26,6 +27,7 @@ export default React.memo(function Input({
26 bind,
27 bind,
27 autoFocus,
28 autoFocus,
28 disabled,
29 disabled,
30+ touched,
29
31
30 // ignored
32 // ignored
31 initialValue,
33 initialValue,
@@ -33,6 +35,7 @@ export default React.memo(function Input({
33 transformers,
35 transformers,
34
36
35 // extra
37 // extra
38+ textarea = false,
36 ...rest
39 ...rest
37}) {
40}) {
38 // NB(shrugs): we disable exhaustive deps because we don't want the callbacks
41 // NB(shrugs): we disable exhaustive deps because we don't want the callbacks
@@ -67,8 +70,11 @@ export default React.memo(function Input({
67 </Flex.Item>
70 </Flex.Item>
68 <Flex.Item as={Flex} className="rel" row>
71 <Flex.Item as={Flex} className="rel" row>
69 <Flex.Item
72 <Flex.Item
70− as="input"
73+ as={type === 'textarea' ? 'textarea' : 'input'}
74+ type={type === 'textarea' ? undefined : type}
71 {...rest}
75 {...rest}
76+ // NOTE: 24px = 12px * 2 (from p3 styling)
77+ style={type === 'textarea' ? { minHeight: 'calc(1rem + 24px)' } : {}}
72 className={cn(
78 className={cn(
73 'b b1 p3 outline-none',
79 'b b1 p3 outline-none',
74 { mono },
80 { mono },
@@ -77,8 +83,8 @@ export default React.memo(function Input({
77 'bg-gray1': disabled,
83 'bg-gray1': disabled,
78 },
84 },
79 {
85 {
80− gray4: !focused,
86+ gray4: !focused && !touched,
81− black: focused,
87+ black: focused || touched,
82 },
88 },
83 {
89 {
84 'b-green3': visiblyPassed,
90 'b-green3': visiblyPassed,
src/indigo-react/components/SelectInput.js+1
@@ -31,6 +31,7 @@ export default function SelectInput({
31 autoFocus,
31 autoFocus,
32 disabled,
32 disabled,
33 options,
33 options,
34+ touched,
34
35
35 // ignored
36 // ignored
36 initialValue,
37 initialValue,
src/indigo-react/components/ToggleInput.js+1
@@ -25,6 +25,7 @@ export default function ToggleInput({
25 bind,
25 bind,
26 autoFocus,
26 autoFocus,
27 disabled,
27 disabled,
28+ touched,
28
29
29 // ignored
30 // ignored
30 initialValue,
31 initialValue,
src/indigo-react/lib/useForm.js+2
@@ -217,6 +217,7 @@ export default function useForm(inputConfigs = []) {
217 error: errors[i],
217 error: errors[i],
218 hintError: hintErrors[i],
218 hintError: hintErrors[i],
219 focused: focuses[i],
219 focused: focuses[i],
220+ touched: hasBeenTouched[name],
220 autoFocus: autoFocus && !disabled,
221 autoFocus: autoFocus && !disabled,
221 disabled,
222 disabled,
222 ...rest,
223 ...rest,
@@ -241,6 +242,7 @@ export default function useForm(inputConfigs = []) {
241 errors,
242 errors,
242 hintErrors,
243 hintErrors,
243 focuses,
244 focuses,
245+ hasBeenTouched,
244 onChange,
246 onChange,
245 onFocus,
247 onFocus,
246 onBlur,
248 onBlur,
src/lib/useInputs.js+15−11
@@ -1,7 +1,6 @@
1import React, { useCallback, useMemo } from 'react';
1import React, { useCallback, useMemo } from 'react';
2import { AccessoryIcon, useForm } from 'indigo-react';
2import { AccessoryIcon, useForm } from 'indigo-react';
3import { identity } from 'lodash';
3import { identity } from 'lodash';
4−import * as bip39 from 'bip39';
5
4
6import { DEFAULT_HD_PATH } from 'lib/wallet';
5import { DEFAULT_HD_PATH } from 'lib/wallet';
7import {
6import {
@@ -22,8 +21,14 @@ import { prependSig, convertToNumber } from 'lib/transformers';
22
21
23import InputSigil from 'components/InputSigil';
22import InputSigil from 'components/InputSigil';
24
23
25−const EXAMPLE_PRIVATE_KEY =
24+const PLACEHOLDER_POINT = '~sampel-ponnym';
26− '0xa44de2416ee6beb2f323fab48b432925c9785808d33a6ca6d7ba00b45e9370c3';
25+const PLACEHOLDER_HD_PATH = DEFAULT_HD_PATH;
26+const PLACEHOLDER_MNEMONIC =
27+ 'example crew supreme gesture quantum web media hazard theory mercy wing kitten';
28+const PLACEHOLDER_TICKET = '~sampel-ticlyt-migfun-falmel';
29+const PLACEHOLDER_ADDRESS = '0x12345abcdeDB11D175F123F6891AA64F01c24F7d';
30+const PLACEHOLDER_PRIVATE_KEY =
31+ '0x12345abcdee6beb2f323fab48b432925c9785808d33a6ca6d7ba00b45e9370c3';
27
32
28// pulls out the first input from a useForm() call
33// pulls out the first input from a useForm() call
29function useFirstOf({ inputs, setValue, ...rest }, mapper = identity) {
34function useFirstOf({ inputs, setValue, ...rest }, mapper = identity) {
@@ -71,7 +76,7 @@ export function useHexInput({ length, ...rest }) {
71 {
76 {
72 type: 'text', // or password
77 type: 'text', // or password
73 autoComplete: 'off',
78 autoComplete: 'off',
74− placeholder: `e.g. ${EXAMPLE_PRIVATE_KEY}`,
79+ placeholder: PLACEHOLDER_PRIVATE_KEY,
75 validators: useMemo(
80 validators: useMemo(
76 () => [
81 () => [
77 validateHexString,
82 validateHexString,
@@ -86,15 +91,14 @@ export function useHexInput({ length, ...rest }) {
86 );
91 );
87}
92}
88
93
89−const kExampleMnemonic = bip39.generateMnemonic();
90const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
94const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
91export function useMnemonicInput(props) {
95export function useMnemonicInput(props) {
92 return useFirstOf(
96 return useFirstOf(
93 useForm([
97 useForm([
94 {
98 {
95− type: 'text',
99+ type: 'textarea',
96 autoComplete: 'off',
100 autoComplete: 'off',
97− placeholder: `e.g. ${kExampleMnemonic}`,
101+ placeholder: PLACEHOLDER_MNEMONIC,
98 validators: kMnemonicValidators,
102 validators: kMnemonicValidators,
99 ...props,
103 ...props,
100 },
104 },
@@ -108,7 +112,7 @@ export function useHdPathInput(props) {
108 {
112 {
109 type: 'text',
113 type: 'text',
110 autoComplete: 'off',
114 autoComplete: 'off',
111− placeholder: `e.g. ${DEFAULT_HD_PATH}`,
115+ placeholder: PLACEHOLDER_HD_PATH,
112 ...props,
116 ...props,
113 },
117 },
114 ])
118 ])
@@ -124,7 +128,7 @@ export function useTicketInput({ validators = [], deriving = false, ...rest }) {
124 {
128 {
125 type: 'password',
129 type: 'password',
126 label: 'Ticket',
130 label: 'Ticket',
127− placeholder: 'e.g. ~ragtyd-modwen-bostec-hinsep',
131+ placeholder: PLACEHOLDER_TICKET,
128 validators: useMemo(() => [...validators, ...kTicketValidators], [
132 validators: useMemo(() => [...validators, ...kTicketValidators], [
129 validators,
133 validators,
130 ]),
134 ]),
@@ -162,7 +166,7 @@ export function usePointInput({ size = 4, validators = [], ...rest }) {
162 {
166 {
163 type: 'text',
167 type: 'text',
164 label: 'Point',
168 label: 'Point',
165− placeholder: 'e.g. ~zod',
169+ placeholder: PLACEHOLDER_POINT,
166 validators: _validators,
170 validators: _validators,
167 transformers: kPointTransformers,
171 transformers: kPointTransformers,
168 mono: true,
172 mono: true,
@@ -236,7 +240,7 @@ export function useAddressInput({ ...rest }) {
236 {
240 {
237 type: 'string',
241 type: 'string',
238 label: 'Ethereum Address',
242 label: 'Ethereum Address',
239− placeholder: 'e.g. 0x6DEfFb0caFDB11D175F123F6891AA64F01c24F7d',
243+ placeholder: PLACEHOLDER_ADDRESS,
240 autoComplete: 'off',
244 autoComplete: 'off',
241 validators: kAddressValidators,
245 validators: kAddressValidators,
242 mono: true,
246 mono: true,
src/lib/useLocalStorageState.js+15−10
@@ -1,4 +1,4 @@
1−import { useState, useEffect } from 'react';
1+import { useCallback, useState } from 'react';
2
2
3/**
3/**
4 * useLocalStorageState is useState but it persists to localStorage
4 * useLocalStorageState is useState but it persists to localStorage
@@ -8,7 +8,7 @@ import { useState, useEffect } from 'react';
8 * https://usehooks.com/useLocalStorage/
8 * https://usehooks.com/useLocalStorage/
9 */
9 */
10export default function useLocalStorageState(key, initialValue) {
10export default function useLocalStorageState(key, initialValue) {
11− const [state, setState] = useState(() => {
11+ const [state, _setState] = useState(() => {
12 try {
12 try {
13 const value = localStorage.getItem(key);
13 const value = localStorage.getItem(key);
14 if (value === null) {
14 if (value === null) {
@@ -25,14 +25,19 @@ export default function useLocalStorageState(key, initialValue) {
25 }
25 }
26 });
26 });
27
27
28− useEffect(() => {
28+ const setState = useCallback(
29+ value => {
29− try {
30+ try {
30− localStorage.setItem(key, JSON.stringify(state));
31+ localStorage.setItem(key, JSON.stringify(value));
31− } catch {
32+ } catch {
32− // if user is in private mode or has storage restriction
33+ // if user is in private mode or has storage restriction
33− // localStorage can throw. Also JSON.stringify can throw.
34+ // localStorage can throw. Also JSON.stringify can throw.
34− }
35+ }
36+
37+ _setState(value);
38+ },
35− }, [key, state]);
39+ [key]
40+ );
36
41
37 return [state, setState];
42 return [state, setState];
38}
43}
src/views/Activate/ActivateCode.js+1−1
@@ -40,7 +40,7 @@ export default function ActivateCode() {
40
40
41 const [ticketInput, { pass: validTicket, data: ticket }] = useTicketInput({
41 const [ticketInput, { pass: validTicket, data: ticket }] = useTicketInput({
42 name: 'ticket',
42 name: 'ticket',
43− label: 'Code',
43+ label: 'Activation Code',
44 initialValue: impliedTicket || '',
44 initialValue: impliedTicket || '',
45 autoFocus: true,
45 autoFocus: true,
46 });
46 });
src/views/Admin/AdminNetworkingKeys.js+4−3
@@ -30,6 +30,8 @@ import { formatDotsWithTime } from 'lib/dateFormat';
30import useEthereumTransaction from 'lib/useEthereumTransaction';
30import useEthereumTransaction from 'lib/useEthereumTransaction';
31import { GAS_LIMITS } from 'lib/constants';
31import { GAS_LIMITS } from 'lib/constants';
32import { addHexPrefix } from 'lib/wallet';
32import { addHexPrefix } from 'lib/wallet';
33+import { useHexInput, useCheckboxInput } from 'lib/useInputs';
34+import useKeyfileGenerator from 'lib/useKeyfileGenerator';
33
35
34import ViewHeader from 'components/ViewHeader';
36import ViewHeader from 'components/ViewHeader';
35import MiniBackButton from 'components/MiniBackButton';
37import MiniBackButton from 'components/MiniBackButton';
@@ -38,8 +40,6 @@ import FooterButton from 'components/FooterButton';
38import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
40import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
39import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
41import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
40import NoticeBox from 'components/NoticeBox';
42import NoticeBox from 'components/NoticeBox';
41−import { useHexInput, useCheckboxInput } from 'lib/useInputs';
42−import useKeyfileGenerator from 'lib/useKeyfileGenerator';
43
43
44const chainKeyProp = name => d =>
44const chainKeyProp = name => d =>
45 d[name] === CURVE_ZERO_ADDR ? Nothing() : Just(d[name]);
45 d[name] === CURVE_ZERO_ADDR ? Nothing() : Just(d[name]);
@@ -330,7 +330,8 @@ export default function AdminNetworkingKeys() {
330 {completed && (
330 {completed && (
331 <>
331 <>
332 <Grid.Item full as={NoticeBox} className="mb3">
332 <Grid.Item full as={NoticeBox} className="mb3">
333− You need this keyfile to authenticate with Arvo.
333+ You need this keyfile to authenticate with Arvo. If you have
334+ already booted Arvo, run <code>|rekey '0wkeyfile.contents'</code>
334 </Grid.Item>
335 </Grid.Item>
335 <Grid.Item full as={DownloadKeyfileButton} solid {...keyfileBind} />
336 <Grid.Item full as={DownloadKeyfileButton} solid {...keyfileBind} />
336 </>
337 </>
src/views/Disclaimer.js+2−2
@@ -20,7 +20,7 @@ export default function ActivateDisclaimer() {
20 label: 'I acknowledge and understand these rights',
20 label: 'I acknowledge and understand these rights',
21 });
21 });
22
22
23− const goToPassport = useCallback(() => {
23+ const goBack = useCallback(async () => {
24 setHasDisclaimed(true);
24 setHasDisclaimed(true);
25 pop();
25 pop();
26 }, [pop, setHasDisclaimed]);
26 }, [pop, setHasDisclaimed]);
@@ -80,7 +80,7 @@ export default function ActivateDisclaimer() {
80 <Grid.Item
80 <Grid.Item
81 as={ForwardButton}
81 as={ForwardButton}
82 disabled={!isUnderstood}
82 disabled={!isUnderstood}
83− onClick={goToPassport}
83+ onClick={goBack}
84 solid
84 solid
85 full>
85 full>
86 Continue
86 Continue
src/views/Login/Advanced.js+4−3
@@ -1,6 +1,7 @@
1import React, { useState } from 'react';
1import React, { useState } from 'react';
2
2
3−import Tabs from 'components/Tabs';
3+import Accordion from 'components/Accordion';
4+
4import PrivateKey from './PrivateKey';
5import PrivateKey from './PrivateKey';
5import Keystore from './Keystore';
6import Keystore from './Keystore';
6
7
@@ -20,10 +21,10 @@ const OPTIONS = [
20];
21];
21
22
22export default function Advanced({ loginCompleted, className }) {
23export default function Advanced({ loginCompleted, className }) {
23− const [currentTab, setCurrentTab] = useState(NAMES.PRIVATE_KEY);
24+ const [currentTab, setCurrentTab] = useState(undefined);
24
25
25 return (
26 return (
26− <Tabs
27+ <Accordion
27 className={className}
28 className={className}
28 views={VIEWS}
29 views={VIEWS}
29 options={OPTIONS}
30 options={OPTIONS}
src/views/Login/Hardware.js+4−3
@@ -1,6 +1,7 @@
1import React, { useState } from 'react';
1import React, { useState } from 'react';
2
2
3−import Tabs from 'components/Tabs';
3+import Accordion from 'components/Accordion';
4+
4import Ledger from './Ledger';
5import Ledger from './Ledger';
5import Trezor from './Trezor';
6import Trezor from './Trezor';
6
7
@@ -20,10 +21,10 @@ const OPTIONS = [
20];
21];
21
22
22export default function Hardware({ loginCompleted, className }) {
23export default function Hardware({ loginCompleted, className }) {
23− const [currentTab, setCurrentTab] = useState(NAMES.LEDGER);
24+ const [currentTab, setCurrentTab] = useState(undefined);
24
25
25 return (
26 return (
26− <Tabs
27+ <Accordion
27 className={className}
28 className={className}
28 views={VIEWS}
29 views={VIEWS}
29 options={OPTIONS}
30 options={OPTIONS}
src/views/Login/Mnemonic.js+2−1
@@ -1,4 +1,5 @@
1import React, { useEffect } from 'react';
1import React, { useEffect } from 'react';
2+import cn from 'classnames';
2import { Just, Nothing } from 'folktale/maybe';
3import { Just, Nothing } from 'folktale/maybe';
3import { Grid, Input, CheckboxInput } from 'indigo-react';
4import { Grid, Input, CheckboxInput } from 'indigo-react';
4
5
@@ -67,7 +68,7 @@ export default function Mnemonic({ className }) {
67 ]);
68 ]);
68
69
69 return (
70 return (
70− <Grid className={className}>
71+ <Grid className={cn('mt4', className)}>
71 <Grid.Item full as={Input} {...mnemonicInput} />
72 <Grid.Item full as={Input} {...mnemonicInput} />
72
73
73 {useAdvanced && (
74 {useAdvanced && (
src/views/Login/Ticket.js+8−2
@@ -1,4 +1,5 @@
1import { Just, Nothing } from 'folktale/maybe';
1import { Just, Nothing } from 'folktale/maybe';
2+import cn from 'classnames';
2import React, { useCallback, useState, useEffect } from 'react';
3import React, { useCallback, useState, useEffect } from 'react';
3import * as azimuth from 'azimuth-js';
4import * as azimuth from 'azimuth-js';
4import * as ob from 'urbit-ob';
5import * as ob from 'urbit-ob';
@@ -88,7 +89,12 @@ export default function Ticket({ className }) {
88 setDeriving(true);
89 setDeriving(true);
89 setUrbitWallet(Nothing());
90 setUrbitWallet(Nothing());
90
91
92+ if (
93+ !ticket ||
94+ !pointName ||
95+ !ob.isValidPatq(ticket) ||
91− if (!ob.isValidPatq(ticket) || !ob.isValidPatp(pointName)) {
96+ !ob.isValidPatp(pointName)
97+ ) {
92 setDeriving(false);
98 setDeriving(false);
93 return;
99 return;
94 }
100 }
@@ -160,7 +166,7 @@ export default function Ticket({ className }) {
160 ]);
166 ]);
161
167
162 return (
168 return (
163− <Grid className={className}>
169+ <Grid className={cn('mt4', className)}>
164 <Grid.Item full as={Input} {...pointInput} />
170 <Grid.Item full as={Input} {...pointInput} />
165
171
166 {!isUsingShards && <Grid.Item full as={Input} {...ticketInput} />}
172 {!isUsingShards && <Grid.Item full as={Input} {...ticketInput} />}
src/views/Point.js+22−11
@@ -10,7 +10,6 @@ import { useHistory } from 'store/history';
10import View from 'components/View';
10import View from 'components/View';
11import Passport from 'components/Passport';
11import Passport from 'components/Passport';
12import { ForwardButton, BootArvoButton } from 'components/Buttons';
12import { ForwardButton, BootArvoButton } from 'components/Buttons';
13−import FooterButton from 'components/FooterButton';
14import { matchBlinky } from 'components/Blinky';
13import { matchBlinky } from 'components/Blinky';
15
14
16import useInvites from 'lib/useInvites';
15import useInvites from 'lib/useInvites';
@@ -52,18 +51,30 @@ export default function Point() {
52 case azimuth.PointSize.Planet:
51 case azimuth.PointSize.Planet:
53 const availableInvitesText = matchBlinky(availableInvites);
52 const availableInvitesText = matchBlinky(availableInvites);
54 return (
53 return (
54+ <>
55+ <Grid.Item
56+ full
57+ as={ForwardButton}
55− <FooterButton disabled={!isActiveOwner} onClick={goInvite}>
58+ disabled={!isActiveOwner}
59+ onClick={goInvite}>
56− Invite <sup>{availableInvitesText} available</sup>
60+ Invite <sup>{availableInvitesText}</sup>
57− </FooterButton>
61+ </Grid.Item>
62+ <Grid.Divider />
63+ </>
58 );
64 );
59 //
65 //
60 case azimuth.PointSize.Star:
66 case azimuth.PointSize.Star:
61 return (
67 return (
62− <FooterButton
68+ <>
69+ <Grid.Item
70+ full
71+ as={ForwardButton}
63− disabled={!isActiveOwner}
72+ disabled={!isActiveOwner}
64− onClick={goPartiesSetPoolSize}>
73+ onClick={goPartiesSetPoolSize}>
65− Manage Parties
74+ Manage Parties
66− </FooterButton>
75+ </Grid.Item>
76+ <Grid.Divider />
77+ </>
67 );
78 );
68 //
79 //
69 default:
80 default:
@@ -78,6 +89,7 @@ export default function Point() {
78 <View>
89 <View>
79 <Passport point={Just(point)} />
90 <Passport point={Just(point)} />
80 <Grid className="pt2">
91 <Grid className="pt2">
92+ {inviteButton}
81 <Grid.Item
93 <Grid.Item
82 full
94 full
83 as={ForwardButton}
95 as={ForwardButton}
@@ -99,9 +111,8 @@ export default function Point() {
99 </>
111 </>
100 )}
112 )}
101 <Grid.Item full as={BootArvoButton} disabled />
113 <Grid.Item full as={BootArvoButton} disabled />
114+ <Grid.Divider />
102 </Grid>
115 </Grid>
103−
104− {inviteButton}
105 </View>
116 </View>
106 );
117 );
107}
118}
src/views/Points.js+12−3
@@ -18,6 +18,7 @@ import FooterButton from 'components/FooterButton';
18import Blinky from 'components/Blinky';
18import Blinky from 'components/Blinky';
19import Passport from 'components/Passport';
19import Passport from 'components/Passport';
20import useRejectedIncomingPointTransfers from 'lib/useRejectedIncomingPointTransfers';
20import useRejectedIncomingPointTransfers from 'lib/useRejectedIncomingPointTransfers';
21+import pluralize from 'lib/pluralize';
21
22
22const getFromMaybe = (obj, key, defaultValue) =>
23const getFromMaybe = (obj, key, defaultValue) =>
23 obj.matchWith({
24 obj.matchWith({
@@ -139,7 +140,11 @@ export default function Points() {
139 {incomingPoints.length > 0 && (
140 {incomingPoints.length > 0 && (
140 <Grid.Item full as={Grid} gap={1} className="mb6">
141 <Grid.Item full as={Grid} gap={1} className="mb6">
141 <Grid.Item full as={H5}>
142 <Grid.Item full as={H5}>
143+ {pluralize(
144+ incomingPoints.length,
145+ 'Incoming Transfer',
142− Incoming Transfers
146+ 'Incoming Transfers'
147+ )}
143 </Grid.Item>
148 </Grid.Item>
144 <Grid.Item
149 <Grid.Item
145 full
150 full
@@ -172,7 +177,11 @@ export default function Points() {
172 {outgoingPoints.length > 0 && (
177 {outgoingPoints.length > 0 && (
173 <Grid.Item full as={Grid} gap={1} className="mb6">
178 <Grid.Item full as={Grid} gap={1} className="mb6">
174 <Grid.Item full as={H5}>
179 <Grid.Item full as={H5}>
180+ {pluralize(
181+ outgoingPoints.length,
182+ 'Outgoing Transfer',
175− Outgoing Transfers
183+ 'Outgoing Transfers'
184+ )}
176 </Grid.Item>
185 </Grid.Item>
177 <Grid.Item
186 <Grid.Item
178 full
187 full
@@ -200,7 +209,7 @@ export default function Points() {
200 {multipassPoints.length > 0 && (
209 {multipassPoints.length > 0 && (
201 <Grid.Item full as={Grid} gap={1}>
210 <Grid.Item full as={Grid} gap={1}>
202 <Grid.Item full as={H5}>
211 <Grid.Item full as={H5}>
203− Multipasses
212+ {pluralize(multipassPoints.length, 'Multipass', 'Multipasses')}
204 </Grid.Item>
213 </Grid.Item>
205 <Grid.Item full as={PointList} points={multipassPoints} />
214 <Grid.Item full as={PointList} points={multipassPoints} />
206 </Grid.Item>
215 </Grid.Item>