on master help sign in
invites: simplified CSV to button now that status is shown earlier, corrected getInvites to be point specific
commit2ecd8f9ef9d42de1e8f64b1bbe935a60eea75f96 authorHunter Miller <hunter@hmiller.dev> dateWed 2 Mar 2022, 17:19 (5 years ago) parent0957ef2
7 files changed+140−183
src/components/L2/Headers/L2PointHeader.tsx+3−2 src/views/Invite/CSVButton.tsx+54 src/views/Invite/CSVModal.tsx−70 src/views/Invite/Cohort.scss+24−50 src/views/Invite/Cohort.tsx+12−18 src/views/Invite/GenerateInvites.tsx+2−2 src/views/Invite/useInvites.ts+45−41
src/components/L2/Headers/L2PointHeader.tsx+3−2
@@ -24,9 +24,10 @@ const L2PointHeader = ({
2424 showMigrate = false,
2525}: L2PointHeaderProps) => {
2626 const { point, pointList } = useRollerStore();
27− const { invites, loading: invitesLoading } = useInviteStore();
27+ const { invites, inviteJobs } = useInviteStore();
2828 const { pointCursor, setPointCursor }: any = usePointCursor();
2929 const numInvites = invites[point.value]?.length;
30+ const loading = inviteJobs[point.value]?.status === 'generating';
3031
3132 const { popTo, push, names }: any = useHistory();
3233
@@ -50,7 +51,7 @@ const L2PointHeader = ({
5051 const renderInviteButton = () => {
5152 if (!showInvites || !point.showInvites) {
5253 return null;
53− } else if (invitesLoading) {
54+ } else if (loading) {
5455 return (
5556 <Row onClick={goToInvites} className="invites">
5657 <LoadingSpinner foreground="rgba(0,0,0,0.3)" background="white" />
src/views/Invite/CSVButton.tsxnew file+54
@@ -0,0 +1,54 @@
1+import { Row, Box, Icon, BaseButton } from '@tlon/indigo-react';
2+
3+import { useEffect, useState } from 'react';
4+import { useInvites } from './useInvites';
5+import { generateCsvName } from 'lib/utils/invite';
6+import { DEFAULT_CSV_NAME } from 'lib/constants';
7+import Point from 'lib/types/Point';
8+
9+interface CSVModalProps {
10+ point: Point;
11+ invitesUpdating: boolean;
12+}
13+
14+export const CSVButton = ({ point, invitesUpdating }: CSVModalProps) => {
15+ const { generateCsv } = useInvites();
16+ const [csv, setCsv] = useState<string | null>(null);
17+ const [error, setError] = useState('');
18+
19+ useEffect(() => {
20+ if (invitesUpdating) {
21+ return;
22+ }
23+
24+ try {
25+ const csv = generateCsv();
26+ setCsv(csv);
27+ } catch (e) {
28+ setError((e as Error).message);
29+ }
30+ }, [generateCsv, invitesUpdating]);
31+
32+ if (!csv || invitesUpdating) {
33+ return (
34+ <BaseButton display="flex" className="download-csv" disabled>
35+ <Icon icon="Download" />
36+ <Box>CSV</Box>
37+ </BaseButton>
38+ );
39+ }
40+
41+ return (
42+ <a
43+ href={`data:text/csv;charset=utf-8,${encodeURIComponent(csv)}`}
44+ download={generateCsvName(
45+ DEFAULT_CSV_NAME,
46+ point.patp?.slice(1) || 'bridge'
47+ )}>
48+ <Row className="download-csv">
49+ <Icon icon="Download" />
50+ <Box>CSV</Box>
51+ </Row>
52+ </a>
53+ );
54+};
src/views/Invite/CSVModal.tsxdeleted−70
@@ -1,70 +0,0 @@
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.scss+24−50
@@ -20,6 +20,15 @@
2020 align-items: center;
2121 cursor: pointer;
2222
23+ &:disabled {
24+ cursor: not-allowed;
25+ color: $light-gray;
26+
27+ svg * {
28+ fill: $light-gray;
29+ }
30+ }
31+
2332 svg {
2433 height: 14px;
2534 width: 20px;
@@ -50,7 +59,7 @@
5059 width: 100%;
5160 font-size: 14px;
5261 line-height: 24px;
53−
62+
5463 &.center {
5564 text-align: center;
5665 margin-top: 20px;
@@ -83,7 +92,8 @@
8392 }
8493 }
8594
86− .copy-invite, .ticket-loader {
95+ .copy-invite,
96+ .ticket-loader {
8797 background-color: $washed-gray;
8898 border-radius: $medium-border-radius;
8999 width: 32px;
@@ -128,7 +138,8 @@
128138
129139 &.show-invite-form {
130140 .body-pane {
131− .upper, .lower {
141+ .upper,
142+ .lower {
132143 width: 100%;
133144 }
134145
@@ -139,14 +150,15 @@
139150 .points-input {
140151 line-height: 28px;
141152 margin-bottom: 24px;
142−
153+
143154 .input-box {
144155 margin: 0 6px;
145156 height: 28px;
146157 width: 40px;
147158 padding: 2px 6px;
148159 font-weight: 800;
149− font-family: "Inter", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji";
160+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, Roboto,
161+ Helvetica, Arial, sans-serif, 'Apple Color Emoji';
150162 text-align: center;
151163
152164 &.over-quota {
@@ -154,22 +166,22 @@
154166 }
155167 }
156168 }
157−
169+
158170 .ship-or-address {
159171 margin: 6px 0;
160172 color: $dark-gray;
161173 }
162−
174+
163175 .dropdown .address {
164176 display: flex;
165177 flex-direction: row;
166178 justify-content: space-between;
167179 align-items: center;
168180 cursor: pointer;
169−
181+
170182 padding: 8px 12px 8px 16px;
171183 }
172−
184+
173185 .next-roll {
174186 width: 100%;
175187 display: flex;
@@ -178,7 +190,7 @@
178190 align-items: center;
179191 font-weight: 500;
180192 }
181−
193+
182194 .generate-codes {
183195 border-radius: $medium-border-radius;
184196 width: 100%;
@@ -186,7 +198,8 @@
186198 font-size: 14px;
187199 background-color: black;
188200 color: white;
189− font-family: "Inter", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji";
201+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, Roboto,
202+ Helvetica, Arial, sans-serif, 'Apple Color Emoji';
190203 border: none;
191204 margin-top: 8px;
192205 padding: 8px;
@@ -220,43 +233,4 @@
220233 font-size: 14px;
221234 line-height: 1.4em;
222235 }
223−
224− .download-csv-modal {
225− height: 167px;
226− width: 333px;
227− display: flex;
228− flex-direction: column;
229− justify-content: space-between;
230− font-weight: 500;
231− max-width: 100%;
232−
233− .download-buttons {
234− justify-content: flex-end;
235−
236− .close-button, .download-button {
237− padding: 4px 16px;
238−
239− &:hover {
240− opacity: 0.8;
241− }
242− }
243−
244− .close-button {
245− color: black;
246− background-color: $washed-gray;
247− margin-right: 8px;
248− }
249− }
250− }
251−
252− .csv-loading {
253− position: absolute;
254− top: 24px;
255− right: 24px;
256−
257− svg {
258− width: 16px;
259− height: 16px;
260− }
261− }
262236}
src/views/Invite/Cohort.tsx+12−18
@@ -1,6 +1,6 @@
11import { useCallback, useState } from 'react';
22import { Button } from 'indigo-react';
3−import { Icon, Row, Box, LoadingSpinner, Text, H3 } from '@tlon/indigo-react';
3+import { Icon, Row, Box, LoadingSpinner, H3 } from '@tlon/indigo-react';
44
55import { useRollerStore } from 'store/rollerStore';
66import { useLocalRouter } from 'lib/LocalRouter';
@@ -20,7 +20,7 @@ import Paginator from 'components/L2/Paginator';
2020
2121import './Cohort.scss';
2222import { useInviteStore } from './useInvites';
23−import { CSVModal } from './CSVModal';
23+import { CSVButton } from './CSVButton';
2424
2525export default function InviteCohort() {
2626 const { names, pop, push } = useLocalRouter();
@@ -28,14 +28,14 @@ export default function InviteCohort() {
2828
2929 const { nextRoll } = useTimerStore();
3030
31− const { invites, loading } = useInviteStore();
31+ const { invites, inviteJobs } = useInviteStore();
3232 const currentL2 = Boolean(point.isL2Spawn);
3333
34− const [showCsvModal, setShowCsvModal] = useState(false);
3534 // TODO: do we need to read this error? currently unused
3635 const [page, setPage] = useState(0);
3736
3837 const invitePoints = invites[point.value];
38+ const inviteJob = inviteJobs[point.value];
3939
4040 const invitesToDisplay = invitePoints.slice(
4141 page * INVITES_PER_PAGE,
@@ -113,11 +113,13 @@ export default function InviteCohort() {
113113 };
114114
115115 const header = () => {
116− if (loading) {
116+ if (inviteJob?.status === 'generating') {
117117 return (
118118 <Row alignItems="center">
119119 <LoadingSpinner dark />
120− <H3 ml={2}>Checking and Updating Invites...</H3>
120+ <H3 ml={2}>
121+ Checking and Updating Invite #{inviteJob.generatingNum}...
122+ </H3>
121123 </Row>
122124 );
123125 }
@@ -149,12 +151,10 @@ export default function InviteCohort() {
149151 ) : (
150152 <Row className="has-invites-header">
151153 {header()}
152− <Row
153− className="download-csv"
154− onClick={() => setShowCsvModal(true)}>
155− <Icon icon="Download" />
156− <Box>CSV</Box>
157− </Row>
154+ <CSVButton
155+ point={point}
156+ invitesUpdating={inviteJob?.status === 'generating'}
157+ />
158158 </Row>
159159 )}
160160 </HeaderPane>
@@ -199,12 +199,6 @@ export default function InviteCohort() {
199199 <strong>{` ${ddmmmYYYY(nextQuotaTime)}`}</strong>.
200200 </Box>
201201 ) : null}
202− <CSVModal
203− point={point.value}
204− show={showCsvModal}
205− hide={() => setShowCsvModal(false)}
206− hideClose
207− />
208202 </View>
209203 );
210204}
src/views/Invite/GenerateInvites.tsx+2−2
@@ -27,7 +27,7 @@ export const GenerateInvites = () => {
2727 const { point, nextQuotaTime } = useRollerStore();
2828 const { getPendingTransactions, getAndUpdatePoint } = useRoller();
2929 const { nextRoll } = useTimerStore();
30− const { generatingNum } = useInviteStore();
30+ const { inviteJobs } = useInviteStore();
3131 const { generateInviteCodes } = useInvites();
3232 const [generatingStatus, setGeneratingStatus] = useState<
3333 InviteGeneratingStatus
@@ -152,7 +152,7 @@ export const GenerateInvites = () => {
152152 </Window>
153153 <GeneratingModal
154154 status={generatingStatus}
155− current={generatingNum}
155+ current={inviteJobs[point.value]?.generatingNum || 0}
156156 total={numInvites}
157157 error={error}
158158 hide={() => {
src/views/Invite/useInvites.ts+45−41
@@ -1,5 +1,5 @@
11import { UnspawnedPoints } from '@urbit/roller-api';
2−import { DEFAULT_CSV_NAME, POINT_DOMINIONS } from 'lib/constants';
2+import { POINT_DOMINIONS } from 'lib/constants';
33import { deriveNetworkSeedFromUrbitWallet } from 'lib/keys';
44import * as need from 'lib/need';
55import * as azimuth from 'azimuth-js';
@@ -25,7 +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';
28+import { generateCsvLine } from 'lib/utils/invite';
2929
3030const inviteTemplate = (
3131 planet: number,
@@ -45,20 +45,24 @@ export type InviteGeneratingStatus =
4545 | 'finished'
4646 | 'errored';
4747
48+interface InviteJob {
49+ generatingNum: number;
50+ status: InviteGeneratingStatus;
51+}
52+
4853interface InviteStore {
4954 invites: Invites;
50− generatingStatus: InviteGeneratingStatus;
51− generatingNum: number;
52− loading: boolean;
55+ inviteJobs: {
56+ [point: number]: InviteJob;
57+ };
5358 addInvite: (point: number, invite: Invite) => void;
5459 setInvites: (points: number, invites: Invite[]) => void;
60+ updateJob: (point: number, updates: Partial<InviteJob>) => void;
5561}
5662
5763export const useInviteStore = create<InviteStore>((set, get) => ({
5864 invites: {},
59− generatingStatus: 'initial',
60− generatingNum: 0,
61− loading: false,
65+ inviteJobs: {},
6266 addInvite: (point: number, invite: Invite) => {
6367 set(state => {
6468 const newInvites: Invites = {};
@@ -76,6 +80,17 @@ export const useInviteStore = create<InviteStore>((set, get) => ({
7680 newInvites[point] = invites;
7781 return { invites: Object.assign(state.invites, newInvites) };
7882 }),
83+ updateJob: (point: number, updates: Partial<InviteJob>) => {
84+ debugger;
85+ const jobs = get().inviteJobs;
86+ const job = jobs[point] || { status: 'initial', generatingNum: 0 };
87+ const newJob = Object.assign({}, job, updates);
88+ jobs[point] = newJob;
89+
90+ //console.log({ point, oldJob: job, updates, newJob });
91+
92+ set({ inviteJobs: jobs });
93+ },
7994}));
8095
8196export function useInvites() {
@@ -87,8 +102,13 @@ export function useInvites() {
87102 const getDetails = allPoints?.getDetails;
88103 const { api, ls } = useRoller();
89104 const { point, pointList, setPendingTransactions } = useRollerStore();
90− const { addInvite, setInvites } = useInviteStore();
91− const updatingInvites = useRef<number[]>([]);
105+ const {
106+ invites,
107+ inviteJobs,
108+ addInvite,
109+ setInvites,
110+ updateJob,
111+ } = useInviteStore();
92112
93113 const generateInviteInfo = async (planet: number, _authToken: string) => {
94114 const { ticket, inviteWallet } = await generateInviteWallet(
@@ -100,13 +120,14 @@ export function useInvites() {
100120 };
101121
102122 const getInvites = useCallback(async () => {
103− if (updatingInvites.current.includes(point.value)) {
123+ const inviteJob = inviteJobs[point.value];
124+ if (inviteJob?.status === 'generating') {
104125 return;
105126 }
106127
107− updatingInvites.current.push(point.value);
108128 try {
109− useInviteStore.setState({ loading: true });
129+ updateJob(point.value, { status: 'generating' });
130+
110131 const curPoint: number = Number(need.point(pointCursor));
111132 const _authToken = authToken.getOrElse(null);
112133 const _contracts = contracts.getOrElse(null);
@@ -145,7 +166,7 @@ export function useInvites() {
145166 const storedInvites = getStoredInvites(ls);
146167
147168 for (let i = 0; i < invitePlanets.length; i++) {
148− useInviteStore.setState({ generatingNum: i + 1 });
169+ updateJob(point.value, { generatingNum: i + 1 });
149170 const planet = invitePlanets[i];
150171 const storedInvite = storedInvites[planet];
151172 const invite =
@@ -169,16 +190,13 @@ export function useInvites() {
169190 }
170191 }
171192
172− useInviteStore.setState({ loading: false, generatingNum: 0 });
193+ updateJob(point.value, { status: 'finished', generatingNum: 0 });
173194 return newInvites;
174195 }
175196 } catch (error) {
176197 console.warn('ERROR GETTING INVITES', error);
177− useInviteStore.setState({ loading: false, generatingNum: 0 });
198+ updateJob(point.value, { status: 'errored', generatingNum: 0 });
178199 }
179−
180− const pointIndex = updatingInvites.current.indexOf(point.value);
181− updatingInvites.current.splice(pointIndex, 1);
182200 }, [
183201 api,
184202 authToken,
@@ -190,6 +208,7 @@ export function useInvites() {
190208 addInvite,
191209 setInvites,
192210 setPendingTransactions,
211+ updateJob,
193212 ]);
194213
195214 const generateInviteCodes = useCallback(
@@ -233,7 +252,7 @@ export function useInvites() {
233252 });
234253
235254 for (let i = 0; i < numInvites && planets[i]; i++) {
236− useInviteStore.setState({ generatingNum: i + 1 });
255+ updateJob(point.value, { generatingNum: i + 1 });
237256 const planet = planets[i];
238257 const nonceInc = i + nonce;
239258
@@ -327,38 +346,23 @@ export function useInvites() {
327346 getDetails,
328347 ls,
329348 getInvites,
349+ updateJob,
330350 ]
331351 );
332352
333− const generateCsv = useCallback(async () => {
334− const invites = await getInvites();
353+ const generateCsv = useCallback(() => {
354+ const pointInvites = invites[point.value];
335355
336− if (!invites) {
356+ if (!pointInvites) {
337357 throw new Error('There was an error creating the CSV');
338358 }
339359
340− const csv = invites.reduce(
360+ return pointInvites.reduce(
341361 (csvData, { ticket, planet }, ind) =>
342362 (csvData += generateCsvLine(ind, ticket, planet)),
343363 'Number,Planet,Invite URL,Point,Ticket\n'
344364 );
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]);
365+ }, [invites, point]);
362366
363367 return {
364368 getInvites,