on eb070bb help sign in
bridge/src/views/Invite/Cohort.js
240 lines 6.3 kB history raw
1import React, { useMemo, useEffect, useCallback, useState } from 'react';
2import { Grid, Flex, Button } from 'indigo-react';
3import * as ob from 'urbit-ob';
4import { Just } from 'folktale/maybe';
5import cn from 'classnames';
7import { usePointCursor } from 'store/pointCursor';
8import { usePointCache } from 'store/pointCache';
10import View from 'components/View';
11import Tabs from 'components/Tabs';
12import BarGraph from 'components/BarGraph';
13import MaybeSigil from 'components/MaybeSigil';
14import { matchBlinky } from 'components/Blinky';
15import Chip from 'components/Chip';
16import Crumbs from 'components/Crumbs';
18import { useLocalRouter } from 'lib/LocalRouter';
19import useInvites from 'lib/useInvites';
20import * as need from 'lib/need';
22import Inviter from 'views/Invite/Inviter';
24import { ReactComponent as SearchIcon } from 'assets/search.svg';
25import NavHeader from 'components/NavHeader';
27function SearchInput({ className, value, onChange }) {
28 return (
29 <Flex align="center" full className={cn('b b-gray2 b1', className)}>
30 <Flex.Item className="p2">
31 <SearchIcon />
32 </Flex.Item>
34 <Flex.Item
35 flex={1}
36 as={'input'}
37 value={value}
38 onChange={onChange}
39 className="b-none h7"
40 />
41 </Flex>
42 );
45function CohortMember({ point, pending = false, className }) {
46 const { sentInvites } = useInvites(point);
47 const patp = useMemo(() => ob.patp(point), [point]);
48 const colors = pending ? ['#ee892b', '#FFFFFF'] : ['#000000', '#FFFFFF'];
50 const DetailText = useCallback(
51 () =>
52 pending ? 'Pending' : <> {matchBlinky(sentInvites)} points invited </>,
53 [pending, sentInvites]
54 );
56 return (
57 <Flex justify="between" className={cn('b1 b-gray2', className)}>
58 <Flex.Item className="w9 h9">
59 <MaybeSigil patp={Just(patp)} size={64} colors={colors} />
60 </Flex.Item>
62 <Flex.Item
63 flex={1}
64 justify="evenly"
65 as={Flex}
66 col
67 className="mono f6 ph4">
68 <Flex.Item>{patp}</Flex.Item>
69 <Flex.Item className="gray4">
70 <DetailText />
71 </Flex.Item>
72 </Flex.Item>
73 </Flex>
74 );
76function CohortList({ acceptedPoints, pendingPoints, className, onlyPending }) {
77 const { syncInvites } = usePointCache();
79 const [query, setQuery] = useState('');
81 useEffect(() => {
82 acceptedPoints.map(syncInvites);
83 }, [acceptedPoints, syncInvites]);
85 const handleChange = useCallback(
86 e => {
87 setQuery(e.target.value);
88 e.preventDefault();
89 },
90 [setQuery]
91 );
93 const filterPoints = useCallback(
94 points =>
95 points.filter(p => {
96 const patp = ob.patp(p).slice(1);
97 return patp.startsWith(query);
98 }),
99 [query]
100 );
102 const _pendingPoints = filterPoints(pendingPoints);
103 const _acceptedPoints = filterPoints(acceptedPoints);
105 const offset = onlyPending ? 0 : _acceptedPoints.length;
107 return (
108 <Grid gap={3} className={cn('mt4', className)}>
109 <Grid.Item full as={SearchInput} value={query} onChange={handleChange} />
110 <>
111 {!onlyPending &&
112 _acceptedPoints.map((p, idx) => (
113 <Grid.Item
114 key={p}
115 half={(idx % 2) + 1}
116 as={CohortMember}
117 point={p}
118 />
119 ))}
120 {_pendingPoints.map((p, idx) => (
121 <Grid.Item
122 key={p}
123 half={((offset + idx) % 2) + 1}
124 as={CohortMember}
125 point={p}
126 pending
127 />
128 ))}
129 </>
131 {_pendingPoints.length === 0 &&
132 (_acceptedPoints.length === 0 || onlyPending) && (
133 <Grid.Item full className="p4 t-center">
134 {' '}
135 No invites accepted yet.
136 </Grid.Item>
137 )}
138 </Grid>
139 );
142export default function InviteCohort() {
143 const { pop } = useLocalRouter();
145 const { pointCursor } = usePointCursor();
146 const point = need.point(pointCursor);
148 const {
149 acceptedInvites,
150 acceptedPoints,
151 pendingPoints,
152 availableInvites,
153 sentInvites,
154 } = useInvites(point);
156 const [showInviteForm, setShowInviteForm] = useState(false);
157 const [tab, setTab] = useState(NAMES.ALL);
159 const _acceptedPoints = acceptedPoints.getOrElse([]);
160 const _pendingPoints = pendingPoints.getOrElse([]);
161 const _pendingInvites = _pendingPoints.length;
162 const _acceptedInvites = _acceptedPoints.length;
163 const _totalInvites = availableInvites
164 .map(a => sentInvites.getOrElse(0) + a)
165 .getOrElse(0);
167 return (
168 <View pop={pop} inset>
169 <NavHeader>
170 <Crumbs
171 className="mb1"
172 routes={[
173 { text: ob.patp(point), action: () => pop() },
174 { text: 'Invite Group' },
175 ]}
176 />
177 </NavHeader>
178 <Grid gap={3}>
179 <Grid.Item full>Invite Group</Grid.Item>
180 <Grid.Item full>
181 <Flex align="center">
182 <Flex.Item>
183 {_acceptedInvites} / {_totalInvites}
184 </Flex.Item>
185 {_pendingInvites !== 0 && (
186 <Flex.Item as={Chip} className="bg-yellow4 white">
187 {_pendingInvites} pending
188 </Flex.Item>
189 )}
190 </Flex>
191 </Grid.Item>
192 <Grid.Item
193 full
194 as={BarGraph}
195 available={availableInvites}
196 sent={sentInvites}
197 accepted={acceptedInvites}
198 />
199 {availableInvites.getOrElse(0) !== 0 && !showInviteForm && (
200 <Grid.Item
201 full
202 solid
203 center
204 as={Button}
205 onClick={() => setShowInviteForm(true)}>
206 Add Members
207 </Grid.Item>
208 )}
209 {showInviteForm && <Inviter />}
211 <Grid.Item
212 full
213 center
214 as={Tabs}
215 views={VIEWS}
216 options={OPTIONS}
217 currentTab={tab}
218 onTabChange={setTab}
219 acceptedPoints={_acceptedPoints}
220 pendingPoints={_pendingPoints}
221 onlyPending={tab === NAMES.PENDING}
222 />
223 </Grid>
224 </View>
225 );
227const NAMES = {
228 ALL: 'ALL',
229 PENDING: 'PENDING',
230};
232const VIEWS = {
233 [NAMES.PENDING]: CohortList,
234 [NAMES.ALL]: CohortList,
235};
237const OPTIONS = [
238 { text: 'All', value: NAMES.ALL },
239 { text: 'Pending', value: NAMES.PENDING },
240];