@@ -1,8 +1,12 @@
11import React, { useEffect, useCallback, useState } from 'react';
22import { Grid, Button } from 'indigo-react';
3−import cn from 'classnames';
43import * as azimuth from 'azimuth-js';
5−import { Icon, Row, StatelessTextInput } from '@tlon/indigo-react';
4+import {
5+ Icon,
6+ LoadingSpinner,
7+ Row,
8+ StatelessTextInput,
9+} from '@tlon/indigo-react';
610
711import { usePointCursor } from 'store/pointCursor';
812import { usePointCache } from 'store/pointCache';
@@ -14,7 +18,11 @@ import { useLocalRouter } from 'lib/LocalRouter';
1418import * as need from 'lib/need';
1519import * as wg from 'lib/walletgen';
1620import useRoller from 'lib/useRoller';
17−import { generateUrl } from 'lib/utils/invite';
21+import {
22+ generateUrl,
23+ generateCsvLine,
24+ generateCsvName,
25+} from 'lib/utils/invite';
1826
1927import { useIssueChild } from 'views/Point/IssueChild';
2028import View from 'components/View';
@@ -29,26 +37,40 @@ import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
2937
3038import './Cohort.scss';
3139import { getStoredInvites } from 'store/storage/roller';
32−import { ETH_ZERO_ADDR } from 'lib/constants';
40+import {
41+ DEFAULT_NUM_INVITES,
42+ ETH_ZERO_ADDR,
43+ INVITES_PER_PAGE,
44+ DEFAULT_CSV_NAME,
45+} from 'lib/constants';
3346import { isPlanet } from 'lib/utils/point';
3447
35−const INVITES_PER_PAGE = 7;
36−const DEFAULT_NUM_INVITES = 5;
48+interface L1Invite {
49+ ticket: string;
50+ planet: number;
51+}
3752
3853export default function InviteCohort() {
39− const { pop } = useLocalRouter();
40− const { authToken } = useWallet();
54+ const { pop }: any = useLocalRouter();
55+ const { authToken }: any = useWallet();
4156 const {
4257 nextRoll,
4358 currentL2,
4459 pendingTransactions,
4560 invites,
61+ invitePoints,
62+ setInviteGeneratingNum,
4663 setInvites,
64+ // inviteGeneratingNum,
4765 } = useRollerStore();
48− const { generateInviteCodes, getPendingTransactions } = useRoller();
49− const { pointCursor } = usePointCursor();
50− const { contracts } = useNetwork();
51− const { syncControlledPoints } = usePointCache();
66+ const {
67+ generateInviteCodes,
68+ getPendingTransactions,
69+ getInvites,
70+ } = useRoller();
71+ const { pointCursor }: any = usePointCursor();
72+ const { contracts }: any = useNetwork();
73+ const { syncControlledPoints }: any = usePointCache();
5274
5375 const point = need.point(pointCursor);
5476 const _contracts = need.contracts(contracts);
@@ -58,11 +80,19 @@ export default function InviteCohort() {
5880 );
5981 const [showInviteForm, setShowInviteForm] = useState(false);
6082 const [loading, setLoading] = useState(false);
61− const [error, setError] = useState(null);
62− const [l1Invite, setL1Invite] = useState(null);
83+ const [error, setError] = useState('');
84+ const [downloadingCsv, setDownloadingCsv] = useState(false);
85+ const [l1Invite, setL1Invite] = useState<L1Invite | null>(null);
86+ const [page, setPage] = useState(0);
6387
6488 const { construct, unconstruct, completed, bind } = useIssueChild();
6589
90+ useEffect(() => {
91+ if (invitePoints.length > 0) {
92+ getInvites(currentL2, page);
93+ }
94+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
95+
6696 // Set up the invite spawn if on L1
6797 useEffect(() => {
6898 const _authToken = authToken.getOrElse(null);
@@ -119,26 +149,24 @@ export default function InviteCohort() {
119149 ]);
120150
121151 useEffect(() => {
122− const { available } = getStoredInvites(point);
152+ const storedInvites = getStoredInvites(point);
123153
124154 if (
125155 !completed ||
126156 !l1Invite ||
127− available.find(({ planet }) => planet === l1Invite.planet)
157+ storedInvites.find(({ planet }) => planet === l1Invite.planet)
128158 ) {
129159 return;
130160 }
131161
132162 syncControlledPoints();
133− setInvites([...invites, l1Invite]);
163+ setInvites([...invites, { ...l1Invite, hash: '', owner: '' }]);
134164 setShowInviteForm(false);
135165 setL1Invite(null);
136166 unconstruct();
137167 pop();
138168 }, [completed]); // eslint-disable-line react-hooks/exhaustive-deps
139169
140− const [page, setPage] = useState(0);
141−
142170 const invitesToDisplay = invites.slice(
143171 page * INVITES_PER_PAGE,
144172 (page + 1) * INVITES_PER_PAGE
@@ -151,16 +179,64 @@ export default function InviteCohort() {
151179 setLoading(true);
152180 try {
153181 await generateInviteCodes(numInvites);
154− getPendingTransactions(point);
182+ getPendingTransactions();
155183 pop();
156184 } catch (error) {
157185 console.log(error);
158186 // TODO: error is not used anywhere on the UI
159− setError(error);
187+ setError(error as string);
160188 } finally {
161189 setLoading(false);
190+ setInviteGeneratingNum(0);
162191 }
163− }, [generateInviteCodes, numInvites, getPendingTransactions, point, pop]);
192+ }, [
193+ generateInviteCodes,
194+ numInvites,
195+ getPendingTransactions,
196+ pop,
197+ setInviteGeneratingNum,
198+ ]);
199+
200+ const downloadCsv = useCallback(() => {
201+ const generateAndDownload = async () => {
202+ setDownloadingCsv(true);
203+ const invites = await getInvites(currentL2, page, true);
204+ if (invites) {
205+ const csv = invites.reduce(
206+ (csvData, { ticket, planet }) =>
207+ (csvData += generateCsvLine(ticket, planet)),
208+ 'Planet,Invite URL\n'
209+ );
210+ const hiddenElement = document.createElement('a');
211+ hiddenElement.href = `data:text/csv;charset=utf-8,${encodeURIComponent(
212+ csv
213+ )}`;
214+ hiddenElement.target = '_blank';
215+
216+ //provide the name for the CSV file to be downloaded
217+ hiddenElement.download = generateCsvName(DEFAULT_CSV_NAME, point);
218+ hiddenElement.click();
219+ } else {
220+ setError('There was an error creating the CSV');
221+ }
222+ setDownloadingCsv(false);
223+ setInviteGeneratingNum(0);
224+ };
225+ generateAndDownload();
226+ }, [
227+ getInvites,
228+ setError,
229+ setDownloadingCsv,
230+ setInviteGeneratingNum,
231+ currentL2,
232+ page,
233+ point,
234+ ]);
235+
236+ const goNextPage = useCallback(() => {
237+ setPage(page + 1);
238+ getInvites(currentL2, page + 1);
239+ }, [currentL2, getInvites, page]);
164240
165241 const getContent = () => {
166242 if (hasInvites) {
@@ -175,14 +251,20 @@ export default function InviteCohort() {
175251 </div>
176252 <div className="invites">
177253 {invitesToDisplay.map(invite => (
178− <div className="invite" key={invite.ticket}>
254+ <div className="invite" key={invite.planet}>
179255 <div className="invite-url">
180256 {generateUrl(invite.ticket, invite.planet)}
181257 </div>
182− <CopiableWithTooltip
183− text={generateUrl(invite.ticket, invite.planet)}
184− className={cn('copy-invite')}
185− />
258+ {invite.ticket ? (
259+ <CopiableWithTooltip
260+ text={generateUrl(invite.ticket, invite.planet)}
261+ className="copy-invite"
262+ />
263+ ) : (
264+ <div className="ticket-loader">
265+ <LoadingSpinner foreground="black" background="white" />
266+ </div>
267+ )}
186268 </div>
187269 ))}
188270 </div>
@@ -226,6 +308,7 @@ export default function InviteCohort() {
226308 if (showInviteForm) {
227309 return (
228310 <View
311+ pop={() => setShowInviteForm(false)}
229312 className="cohort show-invite-form"
230313 hideBack
231314 header={<L2BackHeader back={() => setShowInviteForm(false)} />}>
@@ -258,15 +341,6 @@ export default function InviteCohort() {
258341 </span>
259342 </div>
260343 )}
261− {/* <div>Destination</div>
262− <div className="ship-or-address">Ship or ethereum address</div>
263− <Dropdown value={dropdownValue}>
264− {points.map(point => (
265− <div onClick={selectPoint(point)} className="address">
266− {ob.patp(point)}
267− </div>
268− ))}
269− </Dropdown> */}
270344 </div>
271345 {/* <Inviter /> */}
272346 <div className="lower">
@@ -285,7 +359,7 @@ export default function InviteCohort() {
285359 disabled={loading}
286360 onClick={createInvites}>
287361 {loading
288− ? 'Generating...'
362+ ? `Generating ${numInvites} invites...`
289363 : `Generate Planet Code${
290364 currentL2 ? `s (${numInvites})` : ''
291365 }`}
@@ -323,10 +397,16 @@ export default function InviteCohort() {
323397 ) : (
324398 <Row className="has-invites-header">
325399 {header}
326− <div className="download-csv" onClick={() => null}>
327− <Icon icon="Download" />
328− <div>CSV</div>
329− </div>
400+ {downloadingCsv ? (
401+ <Row className="download-csv">
402+ Generating {invitePoints.length} codes...
403+ </Row>
404+ ) : (
405+ <Row className="download-csv" onClick={downloadCsv}>
406+ <Icon icon="Download" />
407+ <div>CSV</div>
408+ </Row>
409+ )}
330410 </Row>
331411 )}
332412 </HeaderPane>
@@ -350,7 +430,7 @@ export default function InviteCohort() {
350430 numPerPage={INVITES_PER_PAGE}
351431 numElements={invites.length}
352432 goPrevious={() => setPage(page - 1)}
353− goNext={() => setPage(page + 1)}
433+ goNext={goNextPage}
354434 />
355435 )}
356436 </BodyPane>