@@ -1,8 +1,12 @@
1import React, { useEffect, useCallback, useState } from 'react';
1import React, { useEffect, useCallback, useState } from 'react';
2import { Grid, Button } from 'indigo-react';
2import { Grid, Button } from 'indigo-react';
3−import cn from 'classnames';
4import * as azimuth from 'azimuth-js';
3import * as azimuth from 'azimuth-js';
5−import { Icon, Row, StatelessTextInput } from '@tlon/indigo-react';
9+} from '@tlon/indigo-react';
7import { usePointCursor } from 'store/pointCursor';
11import { usePointCursor } from 'store/pointCursor';
8import { usePointCache } from 'store/pointCache';
12import { usePointCache } from 'store/pointCache';
@@ -14,7 +18,11 @@ import { useLocalRouter } from 'lib/LocalRouter';
14import * as need from 'lib/need';
18import * as need from 'lib/need';
15import * as wg from 'lib/walletgen';
19import * as wg from 'lib/walletgen';
16import useRoller from 'lib/useRoller';
20import useRoller from 'lib/useRoller';
17−import { generateUrl } from 'lib/utils/invite';
25+} from 'lib/utils/invite';
19import { useIssueChild } from 'views/Point/IssueChild';
27import { useIssueChild } from 'views/Point/IssueChild';
20import View from 'components/View';
28import View from 'components/View';
@@ -29,26 +37,40 @@ import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
30import './Cohort.scss';
38import './Cohort.scss';
31import { getStoredInvites } from 'store/storage/roller';
39import { getStoredInvites } from 'store/storage/roller';
32−import { ETH_ZERO_ADDR } from 'lib/constants';
45+} from 'lib/constants';
33import { isPlanet } from 'lib/utils/point';
46import { isPlanet } from 'lib/utils/point';
35−const INVITES_PER_PAGE = 7;
48+interface L1Invite {
36−const DEFAULT_NUM_INVITES = 5;
49+ ticket: string;
38export default function InviteCohort() {
53export default function InviteCohort() {
39− const { pop } = useLocalRouter();
54+ const { pop }: any = useLocalRouter();
40− const { authToken } = useWallet();
55+ const { authToken }: any = useWallet();
43 currentL2,
58 currentL2,
44 pendingTransactions,
59 pendingTransactions,
62+ setInviteGeneratingNum,
46 setInvites,
63 setInvites,
64+ // inviteGeneratingNum,
47 } = useRollerStore();
65 } = useRollerStore();
48− const { generateInviteCodes, getPendingTransactions } = useRoller();
68+ getPendingTransactions,
49− const { pointCursor } = usePointCursor();
71+ const { pointCursor }: any = usePointCursor();
50− const { contracts } = useNetwork();
72+ const { contracts }: any = useNetwork();
51− const { syncControlledPoints } = usePointCache();
73+ const { syncControlledPoints }: any = usePointCache();
53 const point = need.point(pointCursor);
75 const point = need.point(pointCursor);
54 const _contracts = need.contracts(contracts);
76 const _contracts = need.contracts(contracts);
@@ -58,11 +80,19 @@ export default function InviteCohort() {
59 const [showInviteForm, setShowInviteForm] = useState(false);
81 const [showInviteForm, setShowInviteForm] = useState(false);
60 const [loading, setLoading] = useState(false);
82 const [loading, setLoading] = useState(false);
61− const [error, setError] = useState(null);
83+ const [error, setError] = useState('');
84+ const [downloadingCsv, setDownloadingCsv] = useState(false);
62− const [l1Invite, setL1Invite] = useState(null);
85+ const [l1Invite, setL1Invite] = useState<L1Invite | null>(null);
86+ const [page, setPage] = useState(0);
64 const { construct, unconstruct, completed, bind } = useIssueChild();
88 const { construct, unconstruct, completed, bind } = useIssueChild();
91+ if (invitePoints.length > 0) {
92+ getInvites(currentL2, page);
94+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
66 // Set up the invite spawn if on L1
96 // Set up the invite spawn if on L1
67 useEffect(() => {
97 useEffect(() => {
68 const _authToken = authToken.getOrElse(null);
98 const _authToken = authToken.getOrElse(null);
@@ -119,26 +149,24 @@ export default function InviteCohort() {
121 useEffect(() => {
151 useEffect(() => {
122− const { available } = getStoredInvites(point);
152+ const storedInvites = getStoredInvites(point);
125 !completed ||
155 !completed ||
126 !l1Invite ||
156 !l1Invite ||
127− available.find(({ planet }) => planet === l1Invite.planet)
157+ storedInvites.find(({ planet }) => planet === l1Invite.planet)
132 syncControlledPoints();
162 syncControlledPoints();
133− setInvites([...invites, l1Invite]);
163+ setInvites([...invites, { ...l1Invite, hash: '', owner: '' }]);
134 setShowInviteForm(false);
164 setShowInviteForm(false);
135 setL1Invite(null);
165 setL1Invite(null);
136 unconstruct();
166 unconstruct();
138 }, [completed]); // eslint-disable-line react-hooks/exhaustive-deps
168 }, [completed]); // eslint-disable-line react-hooks/exhaustive-deps
140− const [page, setPage] = useState(0);
142 const invitesToDisplay = invites.slice(
170 const invitesToDisplay = invites.slice(
143 page * INVITES_PER_PAGE,
171 page * INVITES_PER_PAGE,
144 (page + 1) * INVITES_PER_PAGE
172 (page + 1) * INVITES_PER_PAGE
@@ -151,16 +179,64 @@ export default function InviteCohort() {
151 setLoading(true);
179 setLoading(true);
153 await generateInviteCodes(numInvites);
181 await generateInviteCodes(numInvites);
154− getPendingTransactions(point);
182+ getPendingTransactions();
156 } catch (error) {
184 } catch (error) {
157 console.log(error);
185 console.log(error);
158 // TODO: error is not used anywhere on the UI
186 // TODO: error is not used anywhere on the UI
159− setError(error);
187+ setError(error as string);
160 } finally {
188 } finally {
161 setLoading(false);
189 setLoading(false);
190+ setInviteGeneratingNum(0);
193+ generateInviteCodes,
163− }, [generateInviteCodes, numInvites, getPendingTransactions, point, pop]);
195+ getPendingTransactions,
197+ setInviteGeneratingNum,
200+ const downloadCsv = useCallback(() => {
201+ const generateAndDownload = async () => {
202+ setDownloadingCsv(true);
203+ const invites = await getInvites(currentL2, page, true);
205+ const csv = invites.reduce(
206+ (csvData, { ticket, planet }) =>
207+ (csvData += generateCsvLine(ticket, planet)),
208+ 'Planet,Invite URL\n'
210+ const hiddenElement = document.createElement('a');
211+ hiddenElement.href = `data:text/csv;charset=utf-8,${encodeURIComponent(
214+ hiddenElement.target = '_blank';
216+ //provide the name for the CSV file to be downloaded
217+ hiddenElement.download = generateCsvName(DEFAULT_CSV_NAME, point);
218+ hiddenElement.click();
220+ setError('There was an error creating the CSV');
222+ setDownloadingCsv(false);
223+ setInviteGeneratingNum(0);
225+ generateAndDownload();
230+ setInviteGeneratingNum,
236+ const goNextPage = useCallback(() => {
238+ getInvites(currentL2, page + 1);
239+ }, [currentL2, getInvites, page]);
165 const getContent = () => {
241 const getContent = () => {
166 if (hasInvites) {
242 if (hasInvites) {
@@ -175,14 +251,20 @@ export default function InviteCohort() {
176 <div className="invites">
252 <div className="invites">
177 {invitesToDisplay.map(invite => (
253 {invitesToDisplay.map(invite => (
178− <div className="invite" key={invite.ticket}>
254+ <div className="invite" key={invite.planet}>
179 <div className="invite-url">
255 <div className="invite-url">
180 {generateUrl(invite.ticket, invite.planet)}
256 {generateUrl(invite.ticket, invite.planet)}
182− <CopiableWithTooltip
259+ <CopiableWithTooltip
183− text={generateUrl(invite.ticket, invite.planet)}
260+ text={generateUrl(invite.ticket, invite.planet)}
184− className={cn('copy-invite')}
261+ className="copy-invite"
264+ <div className="ticket-loader">
265+ <LoadingSpinner foreground="black" background="white" />
@@ -226,6 +308,7 @@ export default function InviteCohort() {
226 if (showInviteForm) {
308 if (showInviteForm) {
311+ pop={() => setShowInviteForm(false)}
229 className="cohort show-invite-form"
312 className="cohort show-invite-form"
231 header={<L2BackHeader back={() => setShowInviteForm(false)} />}>
314 header={<L2BackHeader back={() => setShowInviteForm(false)} />}>
@@ -258,15 +341,6 @@ export default function InviteCohort() {
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">
271 {/* <Inviter /> */}
345 {/* <Inviter /> */}
272 <div className="lower">
346 <div className="lower">
@@ -285,7 +359,7 @@ export default function InviteCohort() {
285 disabled={loading}
359 disabled={loading}
286 onClick={createInvites}>
360 onClick={createInvites}>
288− ? 'Generating...'
362+ ? `Generating ${numInvites} invites...`
289 : `Generate Planet Code${
363 : `Generate Planet Code${
290 currentL2 ? `s (${numInvites})` : ''
364 currentL2 ? `s (${numInvites})` : ''
@@ -323,10 +397,16 @@ export default function InviteCohort() {
324 <Row className="has-invites-header">
398 <Row className="has-invites-header">
401+ <Row className="download-csv">
402+ Generating {invitePoints.length} codes...
326− <div className="download-csv" onClick={() => null}>
405+ <Row className="download-csv" onClick={downloadCsv}>
327− <Icon icon="Download" />
406+ <Icon icon="Download" />
328− <div>CSV</div>
407+ <div>CSV</div>
332 </HeaderPane>
412 </HeaderPane>
@@ -350,7 +430,7 @@ export default function InviteCohort() {
350 numPerPage={INVITES_PER_PAGE}
430 numPerPage={INVITES_PER_PAGE}
351 numElements={invites.length}
431 numElements={invites.length}
352 goPrevious={() => setPage(page - 1)}
432 goPrevious={() => setPage(page - 1)}
353− goNext={() => setPage(page + 1)}
433+ goNext={goNextPage}
356 </BodyPane>
436 </BodyPane>