on master help sign in
invites: moving csv out, refine updating lock, reflect status when updating invites
commit0957ef24358f6fec3e330f1a4d3daa587c5168f8 authorHunter Miller <hunter@hmiller.dev> dateWed 2 Mar 2022, 14:38 (5 years ago) parent7b122b6
4 files changed+143−100
src/components/L2/Headers/L2PointHeader.tsx−1 src/views/Invite/CSVModal.tsx+70 src/views/Invite/Cohort.tsx+35−94 src/views/Invite/useInvites.ts+38−5
src/components/L2/Headers/L2PointHeader.tsx−1
@@ -27,7 +27,6 @@ const L2PointHeader = ({
2727 const { invites, loading: invitesLoading } = useInviteStore();
2828 const { pointCursor, setPointCursor }: any = usePointCursor();
2929 const numInvites = invites[point.value]?.length;
30− debugger;
3130
3231 const { popTo, push, names }: any = useHistory();
3332
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 @@
11import { useCallback, useState } from 'react';
22import { 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';
44
55import { useRollerStore } from 'store/rollerStore';
66import { useLocalRouter } from 'lib/LocalRouter';
7−import {
8− generateUrl,
9− generateUrlAbbreviation,
10− generateCsvLine,
11− generateCsvName,
12−} from 'lib/utils/invite';
13−import { INVITES_PER_PAGE, DEFAULT_CSV_NAME } from 'lib/constants';
7+import { generateUrl, generateUrlAbbreviation } from 'lib/utils/invite';
8+import { INVITES_PER_PAGE } from 'lib/constants';
149import { useTimerStore } from 'store/timerStore';
1510import { Invite } from 'lib/types/Invite';
1611import { ddmmmYYYY } from 'lib/utils/date';
@@ -24,8 +19,8 @@ import CopiableWithTooltip from 'components/copiable/CopiableWithTooltip';
2419import Paginator from 'components/L2/Paginator';
2520
2621import './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';
2924
3025export default function InviteCohort() {
3126 const { names, pop, push } = useLocalRouter();
@@ -33,15 +28,11 @@ export default function InviteCohort() {
3328
3429 const { nextRoll } = useTimerStore();
3530
36− const { invites, generatingNum } = useInviteStore();
37− const { getInvites } = useInvites();
31+ const { invites, loading } = useInviteStore();
3832 const currentL2 = Boolean(point.isL2Spawn);
3933
40− const [loading, setLoading] = useState(false);
4134 const [showCsvModal, setShowCsvModal] = useState(false);
42− const [csvElement, setCsvElement] = useState<HTMLAnchorElement | null>();
4335 // TODO: do we need to read this error? currently unused
44− const [error, setError] = useState('');
4536 const [page, setPage] = useState(0);
4637
4738 const invitePoints = invites[point.value];
@@ -54,40 +45,6 @@ export default function InviteCohort() {
5445 const hasPending = Boolean(pendingTransactions.length);
5546 const hasInvites = Boolean(invitePoints.length);
5647
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−
9148 const goNextPage = useCallback(() => {
9249 setPage(page + 1);
9350 }, [page]);
@@ -155,18 +112,28 @@ export default function InviteCohort() {
155112 );
156113 };
157114
158− const header = (
159− <h5>
160− You have
161− <span className="number-emphasis"> {invitePoints.length} </span>
162− Invite Code{invitePoints.length === 1 ? '' : 's'}
163− </h5>
164− );
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 (
126+ <h5>
127+ You have
128+ <span className="number-emphasis"> {invitePoints.length} </span>
129+ Invite Code{invitePoints.length === 1 ? '' : 's'}
130+ </h5>
131+ );
132+ };
165133
166134 const showGenerateButton = !hasPending && !hasInvites && point.canSpawn;
167135 const showAddMoreButton =
168136 (hasInvites || hasPending) && point.canSpawn && point.l2Quota > 0;
169− const generatingCodesText = `Generating ${generatingNum} of ${invitePoints.length} codes...`;
170137
171138 return (
172139 <View
@@ -178,11 +145,13 @@ export default function InviteCohort() {
178145 <Window>
179146 <HeaderPane>
180147 {!hasInvites ? (
181− header
148+ header()
182149 ) : (
183150 <Row className="has-invites-header">
184− {header}
185− <Row className="download-csv" onClick={generateCsv}>
151+ {header()}
152+ <Row
153+ className="download-csv"
154+ onClick={() => setShowCsvModal(true)}>
186155 <Icon icon="Download" />
187156 <Box>CSV</Box>
188157 </Row>
@@ -230,40 +199,12 @@ export default function InviteCohort() {
230199 <strong>{` ${ddmmmYYYY(nextQuotaTime)}`}</strong>.
231200 </Box>
232201 ) : null}
233− <Modal show={showCsvModal} hide={() => setShowCsvModal(false)} hideClose>
234− <Box className="download-csv-modal">
235− <Box>
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>
202+ <CSVModal
203+ point={point.value}
204+ show={showCsvModal}
205+ hide={() => setShowCsvModal(false)}
206+ hideClose
207+ />
267208 </View>
268209 );
269210}
src/views/Invite/useInvites.ts+38−5
@@ -1,5 +1,5 @@
11import { UnspawnedPoints } from '@urbit/roller-api';
2−import { POINT_DOMINIONS } from 'lib/constants';
2+import { DEFAULT_CSV_NAME, POINT_DOMINIONS } from 'lib/constants';
33import { deriveNetworkSeedFromUrbitWallet } from 'lib/keys';
44import * as need from 'lib/need';
55import * as azimuth from 'azimuth-js';
@@ -25,6 +25,7 @@ import {
2525import { useWallet } from 'store/wallet';
2626import { usePointCache } from 'store/pointCache';
2727import create from 'zustand';
28+import { generateCsvLine, generateCsvName } from 'lib/utils/invite';
2829
2930const inviteTemplate = (
3031 planet: number,
@@ -87,7 +88,7 @@ export function useInvites() {
8788 const { api, ls } = useRoller();
8889 const { point, pointList, setPendingTransactions } = useRollerStore();
8990 const { addInvite, setInvites } = useInviteStore();
90− const updatingInvites = useRef(false);
91+ const updatingInvites = useRef<number[]>([]);
9192
9293 const generateInviteInfo = async (planet: number, _authToken: string) => {
9394 const { ticket, inviteWallet } = await generateInviteWallet(
@@ -99,11 +100,11 @@ export function useInvites() {
99100 };
100101
101102 const getInvites = useCallback(async () => {
102− if (updatingInvites.current) {
103+ if (updatingInvites.current.includes(point.value)) {
103104 return;
104105 }
105106
106− updatingInvites.current = true;
107+ updatingInvites.current.push(point.value);
107108 try {
108109 useInviteStore.setState({ loading: true });
109110 const curPoint: number = Number(need.point(pointCursor));
@@ -176,7 +177,8 @@ export function useInvites() {
176177 useInviteStore.setState({ loading: false, generatingNum: 0 });
177178 }
178179
179− updatingInvites.current = false;
180+ const pointIndex = updatingInvites.current.indexOf(point.value);
181+ updatingInvites.current.splice(pointIndex, 1);
180182 }, [
181183 api,
182184 authToken,
@@ -328,8 +330,39 @@ export function useInvites() {
328330 ]
329331 );
330332
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+
331363 return {
332364 getInvites,
365+ generateCsv,
333366 generateInviteCodes,
334367 };
335368}