@@ -1,40 +1,162 @@
1−import React, { useState } from 'react';
2−import { Grid } from 'indigo-react';
1+import React, { useMemo, useEffect, useCallback, useState } from 'react';
2+import { Grid, Flex } from 'indigo-react';
3+import * as ob from 'urbit-ob';
4+import { Just } from 'folktale/maybe';
5+import cn from 'classnames';
36
4−import Tabs from 'components/Tabs';
7+import { usePointCursor } from 'store/pointCursor';
8+import { usePointCache } from 'store/pointCache';
59
6−import InviteMail from 'views/Invite/Email';
7−import InviteUrl from 'views/Invite/Url';
10+import View from 'components/View';
11+import BarGraph from 'components/BarGraph';
12+import InviteSigilList from 'components/InviteSigilList';
13+import MaybeSigil from 'components/MaybeSigil';
14+import { matchBlinky } from 'components/Blinky';
15+import Chip from 'components/Chip';
16+import Crumbs from 'components/Crumbs';
17+
18+import { useLocalRouter } from 'lib/LocalRouter';
19+import useInvites from 'lib/useInvites';
20+import * as need from 'lib/need';
21+
22+import { ReactComponent as SearchIcon } from 'assets/search.svg';
23+
24+function CohortMember({ point, className }) {
25+ const { sentInvites } = useInvites(point);
26+ const patp = useMemo(() => ob.patp(point), [point]);
27+ const colors = ['#000000', '#FFFFFF'];
28+
29+ return (
30+ <Flex justify="between" className={cn('b1 b-gray2', className)}>
31+ <Flex.Item className="w9 h9">
32+ <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
33+ </Flex.Item>
34+
35+ <Flex.Item
36+ flex={1}
37+ justify="evenly"
38+ as={Flex}
39+ col
40+ className="mono f6 ph4">
41+ <Flex.Item>{patp}</Flex.Item>
42+ <Flex.Item>{matchBlinky(sentInvites)} points invited</Flex.Item>
43+ </Flex.Item>
44+ </Flex>
45+ );
46+}
47+function CohortList({ points, className }) {
48+ const { syncInvites } = usePointCache();
49+
50+ const [query, setQuery] = useState('');
51+
52+ useEffect(() => {
53+ points.map(syncInvites);
54+ }, [points, syncInvites]);
55+
56+ const handleChange = useCallback(
57+ e => {
58+ setQuery(e.target.value);
59+ e.preventDefault();
60+ },
61+ [setQuery]
62+ );
63+
64+ const visiblePoints = useMemo(
65+ () =>
66+ points.filter(p => {
67+ const patp = ob.patp(p).slice(1);
68+ return patp.startsWith(query);
69+ }),
70+ [query, points]
71+ );
872
9−const InviteCohort = () => {
10− const [tab, setTab] = useState(NAMES.MAIL);
1173 return (
12− <Grid.Item
13− full
14− as={Tabs}
15− tabClassName="t-center flex1"
16− className="flex1"
17− views={VIEWS}
18− options={OPTIONS}
19− currentTab={tab}
20− onTabChange={setTab}
21− />
74+ <Grid gap={3} className={cn('mt1', className)}>
75+ <Grid.Item align="center" full as={Flex} className="b b-gray2 b1">
76+ <Flex.Item>
77+ <SearchIcon className="p2" />
78+ </Flex.Item>
79+
80+ <Flex.Item
81+ flex={1}
82+ as={'input'}
83+ value={query}
84+ onChange={handleChange}
85+ className="b-none"
86+ />
87+ </Grid.Item>
88+ <>
89+ {visiblePoints.map((p, idx) => (
90+ <Grid.Item key={p} half={(idx % 2) + 1} as={CohortMember} point={p} />
91+ ))}
92+ </>
93+ </Grid>
2294 );
23−};
95+}
2496
25−const NAMES = {
26− MAIL: 'MAIL',
27− URL: 'URL',
28−};
97+export default function InviteCohort() {
98+ const { pop } = useLocalRouter();
2999
30−const VIEWS = {
31− [NAMES.MAIL]: InviteMail,
32− [NAMES.URL]: InviteUrl,
33−};
100+ const { pointCursor } = usePointCursor();
101+ const point = need.point(pointCursor);
34102
35−const OPTIONS = [
36− { text: 'Email', value: NAMES.MAIL },
37− { text: 'URL', value: NAMES.URL },
38−];
103+ const {
104+ acceptedInvites,
105+ acceptedPoints,
106+ pendingPoints,
107+ availableInvites,
108+ sentInvites,
109+ } = useInvites(point);
39110
40−export default InviteCohort;
111+ const _acceptedPoints = acceptedPoints.getOrElse([]);
112+ const _pendingPoints = pendingPoints.getOrElse([]);
113+ const _pendingInvites = _pendingPoints.length;
114+ const _acceptedInvites = _acceptedPoints.length;
115+ const _totalInvites = availableInvites
116+ .map(a => sentInvites.getOrElse(0) + a)
117+ .getOrElse(0);
118+
119+ return (
120+ <View pop={pop} inset>
121+ <Grid gap={3}>
122+ <Grid.Item
123+ full
124+ as={Crumbs}
125+ className="mb1"
126+ routes={[
127+ { text: ob.patp(point), action: () => pop() },
128+ { text: 'Invite Group' },
129+ ]}
130+ />
131+
132+ <Grid.Item full>Invite Group</Grid.Item>
133+ <Grid.Item full>
134+ <Flex align="center">
135+ <Flex.Item>
136+ {_acceptedInvites + _pendingInvites} / {_totalInvites}
137+ </Flex.Item>
138+ {_pendingInvites && (
139+ <Flex.Item as={Chip} color="yellow">
140+ {_pendingInvites} pending
141+ </Flex.Item>
142+ )}
143+ </Flex>
144+ </Grid.Item>
145+ <Grid.Item
146+ full
147+ as={BarGraph}
148+ available={availableInvites}
149+ sent={sentInvites}
150+ accepted={acceptedInvites}
151+ />
152+ <Grid.Item
153+ full
154+ as={InviteSigilList}
155+ pendingPoints={pendingPoints}
156+ acceptedPoints={acceptedPoints}
157+ />
158+ <Grid.Item full as={CohortList} points={_acceptedPoints} />
159+ </Grid>
160+ </View>
161+ );
162+}