@@ -1,40 +1,162 @@
1−import React, { useState } from 'react';
1+import React, { useMemo, useEffect, useCallback, useState } from 'react';
2−import { Grid } from 'indigo-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';
4−import Tabs from 'components/Tabs';
7+import { usePointCursor } from 'store/pointCursor';
8+import { usePointCache } from 'store/pointCache';
6−import InviteMail from 'views/Invite/Email';
10+import View from 'components/View';
7−import InviteUrl from 'views/Invite/Url';
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';
18+import { useLocalRouter } from 'lib/LocalRouter';
19+import useInvites from 'lib/useInvites';
20+import * as need from 'lib/need';
22+import { ReactComponent as SearchIcon } from 'assets/search.svg';
24+function CohortMember({ point, className }) {
25+ const { sentInvites } = useInvites(point);
26+ const patp = useMemo(() => ob.patp(point), [point]);
27+ const colors = ['#000000', '#FFFFFF'];
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} />
40+ className="mono f6 ph4">
41+ <Flex.Item>{patp}</Flex.Item>
42+ <Flex.Item>{matchBlinky(sentInvites)} points invited</Flex.Item>
47+function CohortList({ points, className }) {
48+ const { syncInvites } = usePointCache();
50+ const [query, setQuery] = useState('');
53+ points.map(syncInvites);
54+ }, [points, syncInvites]);
56+ const handleChange = useCallback(
58+ setQuery(e.target.value);
64+ const visiblePoints = useMemo(
67+ const patp = ob.patp(p).slice(1);
68+ return patp.startsWith(query);
9−const InviteCohort = () => {
10− const [tab, setTab] = useState(NAMES.MAIL);
12− <Grid.Item
74+ <Grid gap={3} className={cn('mt1', className)}>
13− full
75+ <Grid.Item align="center" full as={Flex} className="b b-gray2 b1">
14− as={Tabs}
76+ <Flex.Item>
15− tabClassName="t-center flex1"
16− className="flex1"
77+ <SearchIcon className="p2" />
17− views={VIEWS}
78+ </Flex.Item>
19− currentTab={tab}
80+ <Flex.Item
20− onTabChange={setTab}
81+ flex={1}
84+ onChange={handleChange}
89+ {visiblePoints.map((p, idx) => (
90+ <Grid.Item key={p} half={(idx % 2) + 1} as={CohortMember} point={p} />
25−const NAMES = {
97+export default function InviteCohort() {
26− MAIL: 'MAIL',
98+ const { pop } = useLocalRouter();
30−const VIEWS = {
100+ const { pointCursor } = usePointCursor();
31− [NAMES.MAIL]: InviteMail,
101+ const point = need.point(pointCursor);
32− [NAMES.URL]: InviteUrl,
35−const OPTIONS = [
103+ const {
36− { text: 'Email', value: NAMES.MAIL },
104+ acceptedInvites,
37− { text: 'URL', value: NAMES.URL },
105+ acceptedPoints,
109+ } = useInvites(point);
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)
120+ <View pop={pop} inset>
127+ { text: ob.patp(point), action: () => pop() },
128+ { text: 'Invite Group' },
132+ <Grid.Item full>Invite Group</Grid.Item>
134+ <Flex align="center">
136+ {_acceptedInvites + _pendingInvites} / {_totalInvites}
138+ {_pendingInvites && (
139+ <Flex.Item as={Chip} color="yellow">
140+ {_pendingInvites} pending
148+ available={availableInvites}
150+ accepted={acceptedInvites}
154+ as={InviteSigilList}
155+ pendingPoints={pendingPoints}
156+ acceptedPoints={acceptedPoints}
158+ <Grid.Item full as={CohortList} points={_acceptedPoints} />