@@ -3,7 +3,7 @@ import fileDownload from 'js-file-download';
33import * as React from 'react' ;
44import Helmet from 'react-helmet' ;
55
6- import { Account , Search , Sponsor } from '../../api' ;
6+ import { Account , Search , Sponsor , Emails } from '../../api' ;
77import {
88 HACKATHON_NAME ,
99 IAccount ,
@@ -43,6 +43,9 @@ interface ISearchState {
4343 viewSaved : boolean ;
4444 account ?: IAccount ;
4545 sponsor ?: ISponsor ;
46+ emailModalOpen : boolean ;
47+ emailSending : boolean ;
48+ emailStatus : string ;
4649}
4750
4851class SearchContainer extends React . Component < { } , ISearchState > {
@@ -55,13 +58,54 @@ class SearchContainer extends React.Component<{}, ISearchState> {
5558 searchBar : this . getSearchBarFromQuery ( ) ,
5659 loading : false ,
5760 viewSaved : false ,
61+ emailModalOpen : false ,
62+ emailSending : false ,
63+ emailStatus : '' ,
5864 } ;
5965
6066 this . onFilterChange = this . onFilterChange . bind ( this ) ;
6167 this . triggerSearch = this . triggerSearch . bind ( this ) ;
6268 this . downloadData = this . downloadData . bind ( this ) ;
6369 this . onResetForm = this . onResetForm . bind ( this ) ;
6470 this . onSearchBarChanged = this . onSearchBarChanged . bind ( this ) ;
71+ this . openEmailModal = this . openEmailModal . bind ( this ) ;
72+ this . closeEmailModal = this . closeEmailModal . bind ( this ) ;
73+ this . handleSendEmails = this . handleSendEmails . bind ( this ) ;
74+ this . state = {
75+ ...this . state ,
76+ emailModalOpen : false ,
77+ emailSending : false ,
78+ emailStatus : '' ,
79+ } ;
80+ }
81+ openEmailModal ( ) {
82+ this . setState ( { emailModalOpen : true } ) ;
83+ }
84+
85+ closeEmailModal ( ) {
86+ this . setState ( { emailModalOpen : false , emailStatus : '' } ) ;
87+ }
88+
89+ async handleSendEmails ( status : string ) {
90+ this . setState ( { emailSending : true , emailStatus : status } ) ;
91+ try {
92+ const resp = await Emails . sendAutomatedStatus ( status ) ;
93+ // Axios returns the response directly
94+ const { success, failed } = resp . data . data ;
95+ alert (
96+ `Successfully sent ${ success } ${ status . toLowerCase ( ) } emails` +
97+ ( failed > 0 ? ` (${ failed } failed)` : '' )
98+ ) ;
99+ } catch ( err : any ) {
100+ const message = err ?. data ?. message || err ?. message || err ;
101+ alert ( `Failed to send ${ status . toLowerCase ( ) } emails: ${ message } ` ) ;
102+ } finally {
103+ this . setState ( {
104+ emailSending : false ,
105+ emailModalOpen : false ,
106+ emailStatus : '' ,
107+ } ) ;
108+ }
65109 }
66110
67111 public render ( ) {
@@ -126,6 +170,16 @@ class SearchContainer extends React.Component<{}, ISearchState> {
126170 >
127171 Export Hackers
128172 </ Button >
173+ { account && account . accountType === UserType . STAFF && (
174+ < Button
175+ onClick = { this . openEmailModal }
176+ variant = { ButtonVariant . Secondary }
177+ isOutlined = { true }
178+ style = { { marginLeft : '10px' } }
179+ >
180+ Send Emails
181+ </ Button >
182+ ) }
129183 </ Box >
130184 </ Flex >
131185 </ Box >
@@ -139,6 +193,101 @@ class SearchContainer extends React.Component<{}, ISearchState> {
139193 </ Box >
140194 </ Flex >
141195 </ Box >
196+ { /* Email Modal */ }
197+ { this . state . emailModalOpen && (
198+ < div
199+ style = { {
200+ position : 'fixed' ,
201+ top : 0 ,
202+ left : 0 ,
203+ width : '100vw' ,
204+ height : '100vh' ,
205+ background : 'rgba(0,0,0,0.3)' ,
206+ display : 'flex' ,
207+ alignItems : 'center' ,
208+ justifyContent : 'center' ,
209+ zIndex : 1000 ,
210+ } }
211+ onClick = { this . closeEmailModal }
212+ >
213+ < div
214+ style = { {
215+ background : 'white' ,
216+ padding : '16px 32px 32px' ,
217+ borderRadius : 8 ,
218+ minWidth : 320 ,
219+ boxShadow : '0 2px 16px rgba(0,0,0,0.2)' ,
220+ position : 'relative' ,
221+ } }
222+ onClick = { ( e ) => e . stopPropagation ( ) }
223+ >
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+ } }
259+ >
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+ } }
282+ >
283+ Cancel
284+ </ Button >
285+ </ div >
286+
287+ { this . state . emailSending && < p > Sending emails...</ p > }
288+ </ div >
289+ </ div >
290+ ) }
142291 </ Flex >
143292 ) ;
144293 }
@@ -205,7 +354,10 @@ class SearchContainer extends React.Component<{}, ISearchState> {
205354 this . state . account . accountType === UserType . STAFF
206355 ) {
207356 headers . push ( { label : CONSTANTS . AGE_LABEL , key : 'accountId.age' } ) ;
208- headers . push ( { label : CONSTANTS . PHONE_NUMBER_LABEL , key : 'accountId.age' } ) ;
357+ headers . push ( {
358+ label : CONSTANTS . PHONE_NUMBER_LABEL ,
359+ key : 'accountId.age' ,
360+ } ) ;
209361 headers . push ( { label : 'Resume' , key : 'application.general.URL.resume' } ) ;
210362 headers . push ( { label : 'Github' , key : 'application.general.URL.github' } ) ;
211363 headers . push ( {
@@ -221,9 +373,9 @@ class SearchContainer extends React.Component<{}, ISearchState> {
221373 key : 'application.general.URL.other' ,
222374 } ) ;
223375 headers . push ( {
224- label : 'Number of previous hackathons' ,
225- key : 'application.shortAnswer.previousHackathons'
226- } )
376+ label : 'Number of previous hackathons' ,
377+ key : 'application.shortAnswer.previousHackathons' ,
378+ } ) ;
227379 headers . push ( {
228380 label : CONSTANTS . SKILLS_LABEL ,
229381 key : 'application.shortAnswer.skills' ,
@@ -273,14 +425,20 @@ class SearchContainer extends React.Component<{}, ISearchState> {
273425 label : CONSTANTS . PRONOUN_LABEL ,
274426 key : 'accountId.pronoun' ,
275427 } ) ;
276- headers . push ( { label : CONSTANTS . DIETARY_RESTRICTIONS_LABEL , key : 'accountId.dietaryRestrictions' } ) ;
277- headers . push ( { label : 'Authorize MLH to send emails' , key : 'application.other.sendEmail' } )
428+ headers . push ( {
429+ label : CONSTANTS . DIETARY_RESTRICTIONS_LABEL ,
430+ key : 'accountId.dietaryRestrictions' ,
431+ } ) ;
432+ headers . push ( {
433+ label : 'Authorize MLH to send emails' ,
434+ key : 'application.other.sendEmail' ,
435+ } ) ;
278436 }
279437 const tempHeaders : string [ ] = [ ] ;
280438 headers . forEach ( ( header ) => {
281439 tempHeaders . push ( header . label ) ;
282440 } ) ;
283- const csvData : string [ ] = [ tempHeaders . join ( ',' ) ] ;
441+ const csvData : string [ ] = [ tempHeaders . join ( ',' ) ] ;
284442 this . filter ( ) . forEach ( ( result ) => {
285443 if ( result . selected ) {
286444 const row : string [ ] = [ ] ;
@@ -304,7 +462,11 @@ class SearchContainer extends React.Component<{}, ISearchState> {
304462 }
305463 } ) ;
306464
307- fileDownload ( csvData . join ( '\n' ) , 'hackerData.csv' , 'text/csv;charset=utf-8' ) ;
465+ fileDownload (
466+ csvData . join ( '\n' ) ,
467+ 'hackerData.csv' ,
468+ 'text/csv;charset=utf-8'
469+ ) ;
308470 }
309471
310472 private async triggerSearch ( ) : Promise < void > {
@@ -333,12 +495,15 @@ class SearchContainer extends React.Component<{}, ISearchState> {
333495 }
334496
335497 private onFilterChange ( newFilters : ISearchParameter [ ] ) {
336- this . setState ( {
337- query : newFilters ,
338- } , ( ) => {
339- this . updateQueryURL ( newFilters , this . state . searchBar ) ;
340- this . triggerSearch ( ) ;
341- } ) ;
498+ this . setState (
499+ {
500+ query : newFilters ,
501+ } ,
502+ ( ) => {
503+ this . updateQueryURL ( newFilters , this . state . searchBar ) ;
504+ this . triggerSearch ( ) ;
505+ }
506+ ) ;
342507 }
343508
344509 private onSearchBarChanged ( e : any ) {
0 commit comments