Skip to content

Commit 9fccbb9

Browse files
committed
Better UI for email buttons
1 parent 14f10f9 commit 9fccbb9

File tree

2 files changed

+236
-79
lines changed

2 files changed

+236
-79
lines changed

src/api/emails.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { AxiosPromise, AxiosResponse } from 'axios';
1+
import { AxiosPromise } from 'axios';
22
import API from './api';
33
import APIResponse from './APIResponse';
4+
import axios from 'axios';
45

56
class EmailsAPI {
67
constructor() {
7-
// register /api/email endpoints
88
API.createEntity('email');
99
}
1010

@@ -14,11 +14,23 @@ class EmailsAPI {
1414
*/
1515
public sendAutomatedStatus(
1616
status: string
17-
): AxiosPromise<APIResponse<{ success: number; failed: number }>
18-
> {
19-
// Use create with subURL to hit /email/automated/status/:status
17+
): AxiosPromise<APIResponse<{ success: number; failed: number }>> {
2018
return API.getEndpoint('email').create(undefined, {
2119
subURL: `automated/status/${status}`,
20+
config: { withCredentials: true },
21+
});
22+
}
23+
24+
/**
25+
* Get count of hackers with specified status
26+
* GET /api/email/automated/status/:status/count
27+
*/
28+
public getStatusCount(
29+
status: string
30+
): AxiosPromise<APIResponse<{ count: number }>> {
31+
const baseURL = API.getEndpoint('email')['resourceURL'];
32+
return axios.get(`${baseURL}/automated/status/${status}/count`, {
33+
withCredentials: true,
2234
});
2335
}
2436
}

src/features/Search/Search.tsx

Lines changed: 219 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ interface ISearchState {
4646
emailModalOpen: boolean;
4747
emailSending: boolean;
4848
emailStatus: string;
49+
emailConfirming: boolean;
50+
emailCount: number | null;
51+
emailResult: null | {
52+
ok: boolean;
53+
success: number;
54+
failed: number;
55+
statusLabel: string;
56+
errorMessage?: string;
57+
};
4958
}
5059

5160
class SearchContainer extends React.Component<{}, ISearchState> {
@@ -61,6 +70,9 @@ class SearchContainer extends React.Component<{}, ISearchState> {
6170
emailModalOpen: false,
6271
emailSending: false,
6372
emailStatus: '',
73+
emailConfirming: false,
74+
emailCount: null,
75+
emailResult: null,
6476
};
6577

6678
this.onFilterChange = this.onFilterChange.bind(this);
@@ -70,44 +82,106 @@ class SearchContainer extends React.Component<{}, ISearchState> {
7082
this.onSearchBarChanged = this.onSearchBarChanged.bind(this);
7183
this.openEmailModal = this.openEmailModal.bind(this);
7284
this.closeEmailModal = this.closeEmailModal.bind(this);
73-
this.handleSendEmails = this.handleSendEmails.bind(this);
85+
this.startEmailConfirmation = this.startEmailConfirmation.bind(this);
86+
this.backFromEmailConfirmation = this.backFromEmailConfirmation.bind(this);
87+
this.confirmSendEmails = this.confirmSendEmails.bind(this);
88+
this.closeEmailResult = this.closeEmailResult.bind(this);
7489
this.state = {
7590
...this.state,
7691
emailModalOpen: false,
7792
emailSending: false,
7893
emailStatus: '',
94+
emailConfirming: false,
95+
emailCount: null,
96+
emailResult: null,
7997
};
8098
}
8199
openEmailModal() {
82100
this.setState({ emailModalOpen: true });
83101
}
84102

85103
closeEmailModal() {
86-
this.setState({ emailModalOpen: false, emailStatus: '' });
104+
this.setState({
105+
emailModalOpen: false,
106+
emailStatus: '',
107+
emailConfirming: false,
108+
emailCount: null,
109+
emailSending: false,
110+
emailResult: null,
111+
});
87112
}
88113

89-
async handleSendEmails(status: string) {
90-
this.setState({ emailSending: true, emailStatus: status });
114+
async startEmailConfirmation(status: string) {
91115
try {
92-
const resp = await Emails.sendAutomatedStatus(status);
93-
// Axios returns the response directly
94-
const { success, failed } = resp.data.data;
116+
// Fetch the count of emails to be sent
117+
const countResp = await Emails.getStatusCount(status);
118+
const count = countResp.data.data.count;
119+
this.setState({
120+
emailStatus: status,
121+
emailConfirming: true,
122+
emailCount: typeof count === 'number' ? count : 0,
123+
});
124+
} catch (err: any) {
125+
const message = err?.data?.message || err?.message || err;
95126
alert(
96-
`Successfully sent ${success} ${status.toLowerCase()} emails` +
97-
(failed > 0 ? ` (${failed} failed)` : '')
127+
`Failed to retrieve ${status.toLowerCase()} email count: ${message}`
98128
);
129+
}
130+
}
131+
132+
backFromEmailConfirmation() {
133+
this.setState({
134+
emailConfirming: false,
135+
emailCount: null,
136+
emailStatus: '',
137+
});
138+
}
139+
140+
async confirmSendEmails() {
141+
const { emailStatus } = this.state;
142+
if (!emailStatus) return;
143+
try {
144+
this.setState({ emailSending: true });
145+
const resp = await Emails.sendAutomatedStatus(emailStatus);
146+
const { success, failed } = resp.data.data;
147+
this.setState({
148+
emailResult: {
149+
ok: true,
150+
success,
151+
failed,
152+
statusLabel: emailStatus,
153+
},
154+
});
99155
} catch (err: any) {
100156
const message = err?.data?.message || err?.message || err;
101-
alert(`Failed to send ${status.toLowerCase()} emails: ${message}`);
157+
this.setState({
158+
emailResult: {
159+
ok: false,
160+
success: 0,
161+
failed: 0,
162+
statusLabel: emailStatus,
163+
errorMessage: String(message),
164+
},
165+
});
102166
} finally {
103167
this.setState({
104168
emailSending: false,
105-
emailModalOpen: false,
106-
emailStatus: '',
169+
emailConfirming: false,
107170
});
108171
}
109172
}
110173

174+
private closeEmailResult() {
175+
this.setState({
176+
emailResult: null,
177+
emailModalOpen: false,
178+
emailStatus: '',
179+
emailConfirming: false,
180+
emailCount: null,
181+
emailSending: false,
182+
});
183+
}
184+
111185
public render() {
112186
const { searchBar, account, query, loading, viewSaved } = this.state;
113187
return (
@@ -221,70 +295,141 @@ class SearchContainer extends React.Component<{}, ISearchState> {
221295
}}
222296
onClick={(e) => e.stopPropagation()}
223297
>
224-
<h2>Send Decision Emails</h2>
225-
<div
226-
style={{ display: 'flex', flexDirection: 'column', gap: 12 }}
227-
>
228-
<Button
229-
disabled={this.state.emailSending}
230-
onClick={() => this.handleSendEmails('Accepted')}
231-
style={{
232-
backgroundColor: theme.colors.white,
233-
transition: 'background-color 0.2s',
234-
}}
235-
onMouseEnter={(e) => {
236-
e.currentTarget.style.backgroundColor =
237-
theme.colors.black10;
238-
}}
239-
onMouseLeave={(e) => {
240-
e.currentTarget.style.backgroundColor = theme.colors.white;
241-
}}
242-
>
243-
Send All Acceptance Emails
244-
</Button>
245-
<Button
246-
disabled={this.state.emailSending}
247-
onClick={() => this.handleSendEmails('Declined')}
248-
style={{
249-
backgroundColor: theme.colors.white,
250-
transition: 'background-color 0.2s',
251-
}}
252-
onMouseEnter={(e) => {
253-
e.currentTarget.style.backgroundColor =
254-
theme.colors.black10;
255-
}}
256-
onMouseLeave={(e) => {
257-
e.currentTarget.style.backgroundColor = theme.colors.white;
258-
}}
298+
{this.state.emailResult ? (
299+
<div
300+
style={{ display: 'flex', flexDirection: 'column', gap: 12 }}
259301
>
260-
Send All Declined Emails
261-
</Button>
262-
</div>
263-
<div
264-
style={{
265-
display: 'flex',
266-
justifyContent: 'center',
267-
marginTop: 24,
268-
}}
269-
>
270-
<Button
271-
onClick={this.closeEmailModal}
272-
variant={ButtonVariant.Secondary}
273-
isOutlined={true}
274-
disabled={this.state.emailSending}
275-
onMouseEnter={(e) => {
276-
e.currentTarget.style.backgroundColor =
277-
theme.colors.black10;
278-
}}
279-
onMouseLeave={(e) => {
280-
e.currentTarget.style.backgroundColor = theme.colors.white;
281-
}}
302+
<h2>
303+
{this.state.emailResult.ok ? 'Emails Sent' : 'Send Failed'}
304+
</h2>
305+
{this.state.emailResult.ok ? (
306+
<p style={{ marginTop: 4 }}>
307+
Successfully sent{' '}
308+
<strong>{this.state.emailResult.success}</strong>{' '}
309+
{this.state.emailResult.statusLabel.toLowerCase()} email
310+
{this.state.emailResult.success === 1 ? '' : 's'}
311+
{this.state.emailResult.failed > 0
312+
? ` (${this.state.emailResult.failed} failed)`
313+
: ''}
314+
.
315+
</p>
316+
) : (
317+
<>
318+
<p style={{ marginTop: 4 }}>
319+
Failed to send{' '}
320+
{this.state.emailResult.statusLabel.toLowerCase()}{' '}
321+
emails.
322+
</p>
323+
<p
324+
style={{
325+
color: theme.colors.black60,
326+
whiteSpace: 'pre-wrap',
327+
}}
328+
>
329+
{this.state.emailResult.errorMessage}
330+
</p>
331+
</>
332+
)}
333+
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
334+
<Button onClick={this.closeEmailResult}>Done</Button>
335+
</div>
336+
</div>
337+
) : this.state.emailConfirming ? (
338+
<div
339+
style={{ display: 'flex', flexDirection: 'column', gap: 12 }}
282340
>
283-
Cancel
284-
</Button>
285-
</div>
286-
287-
{this.state.emailSending && <p>Sending emails...</p>}
341+
<h2>Confirm Send</h2>
342+
<p style={{ marginTop: 4 }}>
343+
You are about to send{' '}
344+
<strong>{this.state.emailCount ?? 0}</strong>{' '}
345+
{this.state.emailStatus.toLowerCase()} email
346+
{this.state.emailCount === 1 ? '' : 's'}.
347+
</p>
348+
<p style={{ color: theme.colors.black60, marginTop: 0 }}>
349+
This will email all hackers currently marked as{' '}
350+
<strong>{this.state.emailStatus}</strong>.
351+
</p>
352+
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
353+
<Button
354+
disabled={
355+
this.state.emailSending ||
356+
(this.state.emailCount ?? 0) === 0
357+
}
358+
onClick={this.confirmSendEmails}
359+
variant={ButtonVariant.Primary}
360+
>
361+
{this.state.emailSending ? 'Sending…' : 'Confirm Send'}
362+
</Button>
363+
<Button
364+
onClick={this.backFromEmailConfirmation}
365+
variant={ButtonVariant.Secondary}
366+
isOutlined={true}
367+
disabled={this.state.emailSending}
368+
>
369+
Back
370+
</Button>
371+
</div>
372+
</div>
373+
) : (
374+
<>
375+
<h2>Send Decision Emails</h2>
376+
<div
377+
style={{
378+
display: 'flex',
379+
flexDirection: 'column',
380+
gap: 12,
381+
}}
382+
>
383+
<Button
384+
disabled={this.state.emailSending}
385+
onClick={() => this.startEmailConfirmation('Accepted')}
386+
variant={ButtonVariant.Secondary}
387+
isOutlined={true}
388+
onMouseEnter={(e) => {
389+
e.currentTarget.style.backgroundColor =
390+
theme.colors.black5;
391+
}}
392+
onMouseLeave={(e) => {
393+
e.currentTarget.style.backgroundColor =
394+
theme.colors.white;
395+
}}
396+
>
397+
Send All Acceptance Emails
398+
</Button>
399+
<Button
400+
disabled={this.state.emailSending}
401+
onClick={() => this.startEmailConfirmation('Declined')}
402+
variant={ButtonVariant.Secondary}
403+
isOutlined={true}
404+
onMouseEnter={(e) => {
405+
e.currentTarget.style.backgroundColor =
406+
theme.colors.black5;
407+
}}
408+
onMouseLeave={(e) => {
409+
e.currentTarget.style.backgroundColor =
410+
theme.colors.white;
411+
}}
412+
>
413+
Send All Declined Emails
414+
</Button>
415+
</div>
416+
<div
417+
style={{
418+
display: 'flex',
419+
justifyContent: 'center',
420+
marginTop: 24,
421+
}}
422+
>
423+
<Button
424+
onClick={this.closeEmailModal}
425+
variant={ButtonVariant.Primary}
426+
disabled={this.state.emailSending}
427+
>
428+
Cancel
429+
</Button>
430+
</div>
431+
</>
432+
)}
288433
</div>
289434
</div>
290435
)}

0 commit comments

Comments
 (0)