on master help sign in
invites: moving csv out, refine updating lock, reflect status when updating invites
0957ef2Hunter Miller
src/components/L2/Headers/L2PointHeader.tsx−1
@@ -27,7 +27,6 @@ const L2PointHeader = ({
27 const { invites, loading: invitesLoading } = useInviteStore();
27 const { invites, loading: invitesLoading } = useInviteStore();
28 const { pointCursor, setPointCursor }: any = usePointCursor();
28 const { pointCursor, setPointCursor }: any = usePointCursor();
29 const numInvites = invites[point.value]?.length;
29 const numInvites = invites[point.value]?.length;
30− debugger;
31
30
32 const { popTo, push, names }: any = useHistory();
31 const { popTo, push, names }: any = useHistory();
33
32
src/views/Invite/CSVModal.tsxnew file+70
@@ -0,0 +1,70 @@
1+import { Button } from 'indigo-react';
2+import { Row, Box, LoadingSpinner } from '@tlon/indigo-react';
3+
4+import Modal, { ModalProps } from 'components/L2/Modal';
5+import { useEffect, useState } from 'react';
6+import { useInvites, useInviteStore } from './useInvites';
7+
8+interface CSVModalProps extends Omit<ModalProps, 'children'> {
9+ point: number;
10+}
11+
12+export const CSVModal = ({ show, hide, hideClose, point }: CSVModalProps) => {
13+ const [loading, setLoading] = useState(false);
14+ const [csvElement, setCsvElement] = useState<HTMLAnchorElement | null>();
15+ const [error, setError] = useState('');
16+ const { invites, generatingNum } = useInviteStore();
17+ const { generateCsv } = useInvites();
18+ const invitePoints = invites[point];
19+ const generatingCodesText = `Generating ${generatingNum} of ${invitePoints.length} codes...`;
20+
21+ useEffect(() => {
22+ const generate = async () => {
23+ setLoading(true);
24+
25+ try {
26+ const element = await generateCsv();
27+ setCsvElement(element);
28+ } catch (e) {
29+ setError((e as Error).message);
30+ }
31+
32+ setLoading(false);
33+ };
34+
35+ generate();
36+ }, [generateCsv]);
37+
38+ return (
39+ <Modal show={show} hide={hide} hideClose={hideClose}>
40+ <Box className="download-csv-modal">
41+ <Box>
42+ <Box>Download CSV</Box>
43+ {loading && <Box>{generatingCodesText}</Box>}
44+ </Box>
45+ <Row className="download-buttons">
46+ {/*
47+ // @ts-ignore */}
48+ <Button center className="ph4 close-button" solid onClick={hide}>
49+ Close
50+ </Button>
51+ {/*
52+ // @ts-ignore */}
53+ <Button
54+ center
55+ className="ph4 download-button"
56+ disabled={loading}
57+ solid
58+ onClick={() => csvElement?.click()}>
59+ Download CSV
60+ </Button>
61+ </Row>
62+ </Box>
63+ {loading && (
64+ <Box className="csv-loading">
65+ <LoadingSpinner background="#BCDCFF" foreground="#219DFF" />
66+ </Box>
67+ )}
68+ </Modal>
69+ );
70+};
src/views/Invite/Cohort.tsx+35−94
@@ -1,16 +1,11 @@
1import { useCallback, useState } from 'react';
1import { useCallback, useState } from 'react';
2import { Button } from 'indigo-react';
2import { Button } from 'indigo-react';
3−import { Icon, Row, Box, LoadingSpinner } from '@tlon/indigo-react';
3+import { Icon, Row, Box, LoadingSpinner, Text, H3 } from '@tlon/indigo-react';
4
4
5import { useRollerStore } from 'store/rollerStore';
5import { useRollerStore } from 'store/rollerStore';
6import { useLocalRouter } from 'lib/LocalRouter';
6import { useLocalRouter } from 'lib/LocalRouter';
7−import {
8− generateUrl,
9− generateUrlAbbreviation,
7+import { generateUrl, generateUrlAbbreviation } from 'lib/utils/invite';
10− generateCsvLine,
11− generateCsvName,
12−} from 'lib/utils/invite';
13−import { INVITES_PER_PAGE, DEFAULT_CSV_NAME } from 'lib/constants';
8+import { INVITES_PER_PAGE } from 'lib/constants';
14import { useTimerStore } from 'store/timerStore';
9import { useTimerStore } from 'store/timerStore';
15import { Invite } from 'lib/types/Invite';
10import { Invite } from 'lib/types/Invite';
16import { ddmmmYYYY } from 'lib/utils/date';
11import { ddmmmYYYY } from 'lib/utils/date';
@@ -24,8 +19,8 @@ import CopiableWithTooltip from 'components/copiable/CopiableWithTooltip';
24import Paginator from 'components/L2/Paginator';
19import Paginator from 'components/L2/Paginator';
25
20
26import './Cohort.scss';
21import './Cohort.scss';
27−import Modal from 'components/L2/Modal';
28−import { useInvites, useInviteStore } from './useInvites';
22+import { useInviteStore } from './useInvites';
23+import { CSVModal } from './CSVModal';
29
24
30export default function InviteCohort() {
25export default function InviteCohort() {
31 const { names, pop, push } = useLocalRouter();
26 const { names, pop, push } = useLocalRouter();
@@ -33,15 +28,11 @@ export default function InviteCohort() {
33
28
34 const { nextRoll } = useTimerStore();
29 const { nextRoll } = useTimerStore();
35
30
36− const { invites, generatingNum } = useInviteStore();
31+ const { invites, loading } = useInviteStore();
37− const { getInvites } = useInvites();
38 const currentL2 = Boolean(point.isL2Spawn);
32 const currentL2 = Boolean(point.isL2Spawn);
39
33
40− const [loading, setLoading] = useState(false);
41 const [showCsvModal, setShowCsvModal] = useState(false);
34 const [showCsvModal, setShowCsvModal] = useState(false);
42− const [csvElement, setCsvElement] = useState<HTMLAnchorElement | null>();
43 // TODO: do we need to read this error? currently unused
35 // TODO: do we need to read this error? currently unused
44− const [error, setError] = useState('');
45 const [page, setPage] = useState(0);
36 const [page, setPage] = useState(0);
46
37
47 const invitePoints = invites[point.value];
38 const invitePoints = invites[point.value];
@@ -54,40 +45,6 @@ export default function InviteCohort() {
54 const hasPending = Boolean(pendingTransactions.length);
45 const hasPending = Boolean(pendingTransactions.length);
55 const hasInvites = Boolean(invitePoints.length);
46 const hasInvites = Boolean(invitePoints.length);
56
47
57− const generateCsv = useCallback(() => {
58− const generateCsvElement = async () => {
59− setLoading(true);
60− setShowCsvModal(true);
61− const invites = await getInvites();
62− if (invites) {
63− const csv = invites.reduce(
64− (csvData, { ticket, planet }, ind) =>
65− (csvData += generateCsvLine(ind, ticket, planet)),
66− 'Number,Planet,Invite URL,Point,Ticket\n'
67− );
68− document.getElementById('csv')?.remove();
69− const hiddenElement = document.createElement('a');
70− hiddenElement.href = `data:text/csv;charset=utf-8,${encodeURIComponent(
71− csv
72− )}`;
73− hiddenElement.target = '_blank';
74− hiddenElement.id = 'csv';
75−
76− //provide the name for the CSV file to be downloaded
77− hiddenElement.download = generateCsvName(
78− DEFAULT_CSV_NAME,
79− point.patp?.slice(1) || 'bridge'
80− );
81− setCsvElement(hiddenElement);
82− } else {
83− setError('There was an error creating the CSV');
84− }
85− setLoading(false);
86− useInviteStore.setState({ generatingNum: 0 });
87− };
88− generateCsvElement();
89− }, [getInvites, setError, setLoading, setShowCsvModal, point]);
90−
91 const goNextPage = useCallback(() => {
48 const goNextPage = useCallback(() => {
92 setPage(page + 1);
49 setPage(page + 1);
93 }, [page]);
50 }, [page]);
@@ -155,18 +112,28 @@ export default function InviteCohort() {
155 );
112 );
156 };
113 };
157
114
158− const header = (
115+ const header = () => {
116+ if (loading) {
117+ return (
118+ <Row alignItems="center">
119+ <LoadingSpinner dark />
120+ <H3 ml={2}>Checking and Updating Invites...</H3>
121+ </Row>
122+ );
123+ }
124+
125+ return (
159− <h5>
126+ <h5>
160− You have
127+ You have
161− <span className="number-emphasis"> {invitePoints.length} </span>
128+ <span className="number-emphasis"> {invitePoints.length} </span>
162− Invite Code{invitePoints.length === 1 ? '' : 's'}
129+ Invite Code{invitePoints.length === 1 ? '' : 's'}
163− </h5>
130+ </h5>
131+ );
164− );
132+ };
165
133
166 const showGenerateButton = !hasPending && !hasInvites && point.canSpawn;
134 const showGenerateButton = !hasPending && !hasInvites && point.canSpawn;
167 const showAddMoreButton =
135 const showAddMoreButton =
168 (hasInvites || hasPending) && point.canSpawn && point.l2Quota > 0;
136 (hasInvites || hasPending) && point.canSpawn && point.l2Quota > 0;
169− const generatingCodesText = `Generating ${generatingNum} of ${invitePoints.length} codes...`;
170
137
171 return (
138 return (
172 <View
139 <View
@@ -178,11 +145,13 @@ export default function InviteCohort() {
178 <Window>
145 <Window>
179 <HeaderPane>
146 <HeaderPane>
180 {!hasInvites ? (
147 {!hasInvites ? (
181− header
148+ header()
182 ) : (
149 ) : (
183 <Row className="has-invites-header">
150 <Row className="has-invites-header">
184− {header}
151+ {header()}
152+ <Row
185− <Row className="download-csv" onClick={generateCsv}>
153+ className="download-csv"
154+ onClick={() => setShowCsvModal(true)}>
186 <Icon icon="Download" />
155 <Icon icon="Download" />
187 <Box>CSV</Box>
156 <Box>CSV</Box>
188 </Row>
157 </Row>
@@ -230,40 +199,12 @@ export default function InviteCohort() {
230 <strong>{` ${ddmmmYYYY(nextQuotaTime)}`}</strong>.
199 <strong>{` ${ddmmmYYYY(nextQuotaTime)}`}</strong>.
231 </Box>
200 </Box>
232 ) : null}
201 ) : null}
202+ <CSVModal
203+ point={point.value}
204+ show={showCsvModal}
233− <Modal show={showCsvModal} hide={() => setShowCsvModal(false)} hideClose>
205+ hide={() => setShowCsvModal(false)}
234− <Box className="download-csv-modal">
206+ hideClose
235− <Box>
207+ />
236− <Box>Download CSV</Box>
237− {loading && <Box>{generatingCodesText}</Box>}
238− </Box>
239− <Row className="download-buttons">
240− {/*
241− // @ts-ignore */}
242− <Button
243− center
244− className="ph4 close-button"
245− solid
246− onClick={() => setShowCsvModal(false)}>
247− Close
248− </Button>
249− {/*
250− // @ts-ignore */}
251− <Button
252− center
253− className="ph4 download-button"
254− disabled={loading}
255− solid
256− onClick={() => csvElement?.click()}>
257− Download CSV
258− </Button>
259− </Row>
260− </Box>
261− {loading && (
262− <Box className="csv-loading">
263− <LoadingSpinner background="#BCDCFF" foreground="#219DFF" />
264− </Box>
265− )}
266− </Modal>
267 </View>
208 </View>
268 );
209 );
269}
210}
src/views/Invite/useInvites.ts+38−5
@@ -1,5 +1,5 @@
1import { UnspawnedPoints } from '@urbit/roller-api';
1import { UnspawnedPoints } from '@urbit/roller-api';
2−import { POINT_DOMINIONS } from 'lib/constants';
2+import { DEFAULT_CSV_NAME, POINT_DOMINIONS } from 'lib/constants';
3import { deriveNetworkSeedFromUrbitWallet } from 'lib/keys';
3import { deriveNetworkSeedFromUrbitWallet } from 'lib/keys';
4import * as need from 'lib/need';
4import * as need from 'lib/need';
5import * as azimuth from 'azimuth-js';
5import * as azimuth from 'azimuth-js';
@@ -25,6 +25,7 @@ import {
25import { useWallet } from 'store/wallet';
25import { useWallet } from 'store/wallet';
26import { usePointCache } from 'store/pointCache';
26import { usePointCache } from 'store/pointCache';
27import create from 'zustand';
27import create from 'zustand';
28+import { generateCsvLine, generateCsvName } from 'lib/utils/invite';
28
29
29const inviteTemplate = (
30const inviteTemplate = (
30 planet: number,
31 planet: number,
@@ -87,7 +88,7 @@ export function useInvites() {
87 const { api, ls } = useRoller();
88 const { api, ls } = useRoller();
88 const { point, pointList, setPendingTransactions } = useRollerStore();
89 const { point, pointList, setPendingTransactions } = useRollerStore();
89 const { addInvite, setInvites } = useInviteStore();
90 const { addInvite, setInvites } = useInviteStore();
90− const updatingInvites = useRef(false);
91+ const updatingInvites = useRef<number[]>([]);
91
92
92 const generateInviteInfo = async (planet: number, _authToken: string) => {
93 const generateInviteInfo = async (planet: number, _authToken: string) => {
93 const { ticket, inviteWallet } = await generateInviteWallet(
94 const { ticket, inviteWallet } = await generateInviteWallet(
@@ -99,11 +100,11 @@ export function useInvites() {
99 };
100 };
100
101
101 const getInvites = useCallback(async () => {
102 const getInvites = useCallback(async () => {
102− if (updatingInvites.current) {
103+ if (updatingInvites.current.includes(point.value)) {
103 return;
104 return;
104 }
105 }
105
106
106− updatingInvites.current = true;
107+ updatingInvites.current.push(point.value);
107 try {
108 try {
108 useInviteStore.setState({ loading: true });
109 useInviteStore.setState({ loading: true });
109 const curPoint: number = Number(need.point(pointCursor));
110 const curPoint: number = Number(need.point(pointCursor));
@@ -176,7 +177,8 @@ export function useInvites() {
176 useInviteStore.setState({ loading: false, generatingNum: 0 });
177 useInviteStore.setState({ loading: false, generatingNum: 0 });
177 }
178 }
178
179
179− updatingInvites.current = false;
180+ const pointIndex = updatingInvites.current.indexOf(point.value);
181+ updatingInvites.current.splice(pointIndex, 1);
180 }, [
182 }, [
181 api,
183 api,
182 authToken,
184 authToken,
@@ -328,8 +330,39 @@ export function useInvites() {
328 ]
330 ]
329 );
331 );
330
332
333+ const generateCsv = useCallback(async () => {
334+ const invites = await getInvites();
335+
336+ if (!invites) {
337+ throw new Error('There was an error creating the CSV');
338+ }
339+
340+ const csv = invites.reduce(
341+ (csvData, { ticket, planet }, ind) =>
342+ (csvData += generateCsvLine(ind, ticket, planet)),
343+ 'Number,Planet,Invite URL,Point,Ticket\n'
344+ );
345+
346+ document.getElementById('csv')?.remove();
347+ const hiddenElement = document.createElement('a');
348+ hiddenElement.href = `data:text/csv;charset=utf-8,${encodeURIComponent(
349+ csv
350+ )}`;
351+ hiddenElement.target = '_blank';
352+ hiddenElement.id = 'csv';
353+
354+ //provide the name for the CSV file to be downloaded
355+ hiddenElement.download = generateCsvName(
356+ DEFAULT_CSV_NAME,
357+ point.patp?.slice(1) || 'bridge'
358+ );
359+
360+ return hiddenElement;
361+ }, [getInvites, point]);
362+
331 return {
363 return {
332 getInvites,
364 getInvites,
365+ generateCsv,
333 generateInviteCodes,
366 generateInviteCodes,
334 };
367 };
335}
368}