Skip to content

Commit 14855cf

Browse files
author
Lauren Pothuru
committed
Implement Admin Newsletter backend
- Added API endpoint to send newsletters - Integrated email service to dispatch newsletters - Updated frontend to include newsletter form and submission handling
1 parent a320f2e commit 14855cf

File tree

5 files changed

+650
-334
lines changed

5 files changed

+650
-334
lines changed
Lines changed: 45 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable import/prefer-default-export */
12
import { Resend } from 'resend';
23
import * as dotenv from 'dotenv';
34
import fetch, { Headers, Response, Request } from 'node-fetch';
@@ -6,8 +7,8 @@ import React from 'react';
67
import { ApartmentWithId } from '@common/types/db-types';
78
import GenerateNewsletter from './templates/GenerateNewsletter';
89
import { getUserBatches, USERS } from './helpers/firebase_users_loader';
10+
import { NewsletterRequest } from './templates/Types';
911

10-
// Initialize fetch globals if needed
1112
if (!global.fetch) {
1213
global.fetch = fetch as unknown as typeof global.fetch;
1314
global.Headers = Headers as unknown as typeof global.Headers;
@@ -25,50 +26,31 @@ type EmailCampaignOptions = {
2526
recentAreaPropertyIDs?: string[];
2627
lovedPropertyIDs?: string[];
2728
reviewedPropertyIDs?: string[];
29+
newsletterData?: NewsletterRequest;
2830
};
2931

30-
/**
31-
* sendEmailCampaign
32-
* Sends a marketing email campaign to batches of users featuring apartment properties.
33-
*
34-
* @param options - Configuration options for the email campaign
35-
* @param options.subject - Email subject line (default: 'Check Out These New Apartments!')
36-
* @param options.toEmail - Primary recipient email address (default: '[email protected]')
37-
* @param options.nearbyPropertyIDs - List of property IDs to feature as nearby available properties
38-
* @param options.budgetPropertyIDs - List of property IDs to feature as budget-friendly properties
39-
* @param options.recentAreaPropertyIDs - List of property IDs to feature as recent area properties
40-
* @param options.lovedPropertyIDs - List of property IDs to feature as top loved properties
41-
* @param options.reviewedPropertyIDs - List of property IDs to feature as most reviewed properties
42-
* @returns Promise that resolves when all email batches have been sent
43-
*/
4432
const sendEmailCampaign = async (options: EmailCampaignOptions = {}): Promise<void> => {
45-
const { subject = 'Check Out These New Apartments!', toEmail = '[email protected]' } =
46-
options;
33+
const {
34+
subject = 'Check Out These New Apartments!',
35+
toEmail = '[email protected]',
36+
newsletterData,
37+
} = options;
4738

48-
// Load environment variables
4939
dotenv.config({ path: path.resolve(process.cwd(), '.env.dev') });
5040

51-
const fromEmail = 'updates.cuapts.org';
41+
const fromEmail = 'updates@cuapts.org';
5242
const fromName = 'CU Apts';
5343

5444
const apiKey = process.env.RESEND_API_KEY;
5545
if (!apiKey) {
56-
console.error('Missing RESEND_API_KEY in environment variables');
57-
return;
46+
throw new Error('Missing RESEND_API_KEY in environment variables');
5847
}
5948

60-
/**
61-
* getPropertiesByIds
62-
* Fetches apartment data for a given list of property IDs.
63-
*
64-
* @param ids - List of apartment IDs to fetch from backend API
65-
* @returns List of ApartmentWithId objects
66-
*
67-
*/
6849
const getPropertiesByIds = async (ids: string[]): Promise<ApartmentWithId[]> => {
50+
if (!ids || ids.length === 0) return [];
51+
6952
try {
7053
const idParam = ids.join(',');
71-
7254
const response = await fetch(`${API_BASE_URL}/api/apts/${idParam}`);
7355

7456
if (!response.ok) {
@@ -83,7 +65,7 @@ const sendEmailCampaign = async (options: EmailCampaignOptions = {}): Promise<vo
8365
}
8466
};
8567

86-
// Loads chosen properties
68+
// Load properties
8769
const nearbyProperties = options.nearbyPropertyIDs
8870
? await getPropertiesByIds(options.nearbyPropertyIDs)
8971
: [];
@@ -100,63 +82,50 @@ const sendEmailCampaign = async (options: EmailCampaignOptions = {}): Promise<vo
10082
? await getPropertiesByIds(options.reviewedPropertyIDs)
10183
: [];
10284

103-
console.log(`Fetched ${nearbyProperties.length} nearby properties (recently released spotlight)`);
104-
console.log(`Fetched ${budgetProperties.length} budget properties (recently released spotlight)`);
105-
console.log(`Fetched ${recentAreaProperties.length} recent area properties (area spotlight)`);
106-
console.log(`Fetched ${lovedProperties.length} loved properties (loved spotlight)`);
107-
console.log(`Fetched ${reviewedProperties.length} reviewed properties (loved spotlight)`);
85+
console.log(`Fetched ${nearbyProperties.length} nearby properties`);
86+
console.log(`Fetched ${budgetProperties.length} budget properties`);
87+
console.log(`Fetched ${recentAreaProperties.length} recent area properties`);
88+
console.log(`Fetched ${lovedProperties.length} loved properties`);
89+
console.log(`Fetched ${reviewedProperties.length} reviewed properties`);
10890

10991
const resend = new Resend(apiKey);
11092

111-
/**
112-
* BATCH PROCESSING AND EMAIL SENDING
113-
* Processes users in batches and sends emails concurrently:
114-
* - Creates batches of 50 users and maps over batches to send emails in parallel
115-
* - Uses BCC to hide recipient emails from each other
116-
*
117-
* To use, uncomment line 191, comment out line 192, and run the file as normal.
118-
*/
119-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
12093
const sendBatchEmail = async () => {
12194
try {
122-
console.log(`Total users available in database: ${USERS.length}`);
95+
console.log(`Total users available: ${USERS.length}`);
12396
const validEmails = USERS.filter((user) => user.email && user.email.includes('@'));
12497
console.log(`Valid email addresses: ${validEmails.length}`);
12598

12699
if (validEmails.length === 0) {
127-
console.error('No valid email addresses found!');
128-
return;
100+
throw new Error('No valid email addresses found!');
129101
}
130102

131103
const userBatches = await getUserBatches(50);
132-
console.log(
133-
`Preparing to send emails to ${userBatches.length} batches of users (${50} per batch)`
134-
);
104+
console.log(`Sending to ${userBatches.length} batches of users`);
135105

136106
const emailPromises = userBatches.map(async (batch, i) => {
137107
const bccEmails = batch.map((user) => user.email);
138-
console.log(
139-
`Preparing batch ${i + 1}/${userBatches.length} with ${bccEmails.length} recipients`
140-
);
141108

142109
const { data, error } = await resend.emails.send({
143110
from: `${fromName} <${fromEmail}>`,
144111
to: toEmail,
145-
// bcc: bccEmails,
112+
bcc: bccEmails,
146113
subject,
147114
react: React.createElement(GenerateNewsletter, {
148115
nearbyProperties,
149116
budgetProperties,
150117
recentAreaProperties,
151118
lovedProperties,
152119
reviewedProperties,
120+
newsletterData,
153121
}),
154122
});
155123

156124
if (error) {
157125
console.error(`Error sending batch ${i + 1}:`, error);
126+
throw error;
158127
} else {
159-
console.log(`Batch ${i + 1} sent successfully! ID:`, data?.id || 'no ID returned');
128+
console.log(`Batch ${i + 1} sent successfully! ID:`, data?.id);
160129
}
161130
});
162131

@@ -168,65 +137,46 @@ const sendEmailCampaign = async (options: EmailCampaignOptions = {}): Promise<vo
168137
}
169138
};
170139

171-
/**
172-
* SINGLE TEST EMAIL SENDING
173-
* Sends an email to one person (useful for testing email templates).
174-
* To use, uncomment line 192, comment out line 191, edit info below,
175-
* and run the file as normal.
176-
*/
177140
const sendSingleTestEmail = async () => {
178141
try {
179142
const { data, error } = await resend.emails.send({
180-
181-
143+
from: `${fromName} <${fromEmail}>`,
144+
to: toEmail,
182145
subject,
183146
react: React.createElement(GenerateNewsletter, {
184147
nearbyProperties,
185148
budgetProperties,
186149
recentAreaProperties,
187150
lovedProperties,
188151
reviewedProperties,
152+
newsletterData,
189153
}),
190154
});
155+
191156
if (error) {
192157
console.error('Error sending email:', error);
158+
throw error;
193159
} else {
194-
console.log('Email sent successfully! ID:', data ? data.id : ' no ID returned.');
160+
console.log('Email sent successfully! ID:', data?.id);
195161
}
196162
} catch (err) {
197163
console.error('Exception when sending email:', err);
164+
throw err;
198165
}
199166
};
200167

201-
// sendBatchEmail();
202-
sendSingleTestEmail();
203-
};
204-
205-
/**
206-
* Entry point function that executes the email campaign with default settings.
207-
* Handles logging and error handling for the campaign process.
208-
*
209-
* @returns Promise that resolves when the campaign completes
210-
*/
211-
async function main() {
212-
try {
213-
console.log('Starting email campaign...');
214-
215-
// Customize email subject
216-
await sendEmailCampaign({
217-
subject: 'New Apartment Listings Available!',
218-
recentAreaPropertyIDs: ['12', '2', '24'],
219-
budgetPropertyIDs: ['23', '24', '24'],
220-
nearbyPropertyIDs: ['14', '23', '24'],
221-
reviewedPropertyIDs: ['1', '2', '3'],
222-
lovedPropertyIDs: ['4', '5', '6'],
223-
});
224-
225-
console.log('Campaign completed successfully!');
226-
} catch (error) {
227-
console.error('Failed to send campaign:', error);
168+
// Determine which sending method to use based on sendToAll flag
169+
// If newsletterData exists and sendToAll is true, send to all users
170+
// Otherwise, send a single test email
171+
const shouldSendToAll = newsletterData?.sendToAll === true;
172+
173+
if (shouldSendToAll) {
174+
console.log('Sending to all subscribers...');
175+
await sendBatchEmail();
176+
} else {
177+
console.log(`Sending test email to: ${toEmail}`);
178+
await sendSingleTestEmail();
228179
}
229-
}
180+
};
230181

231-
// Execute main function directly
232-
main().catch(console.error);
182+
export { sendEmailCampaign };

0 commit comments

Comments
 (0)