Files
ocean-cleanup/server/src/services/emailService.ts
Paul Bailey 559976ea85 fix: improve toggle switch styling in MapFilters
- Update all toggle switches to use consistent sizing (h-6 w-11)
- Add proper shadow and border styling
- Improve transition animations
- Remove touch-manipulation styles that were causing layout issues
- Add focus ring states for accessibility
- Make switches look more polished and professional

All toggles now have a clean, modern appearance.
2025-11-17 01:11:10 +00:00

193 lines
5.5 KiB
TypeScript

import axios from 'axios';
// AhaSend API configuration (v2)
const AHASEND_API_KEY = process.env.AHASEND_API_KEY || 'aha-sk-GbzRp0nqVCgmQVw0FN47HNHdFNjR055cQCY1L-l5BSBV9_EmE1-NGHPbR2e2gaR2';
const AHASEND_ACCOUNT_ID = process.env.AHASEND_ACCOUNT_ID || '8a14f9a5-6e6d-47c9-b5d6-a687265b36a3';
const FROM_EMAIL = process.env.FROM_EMAIL || 'Beach Litter Platform <noreply@psbailey.uk>';
interface EmailData {
to: string;
subject: string;
text: string;
html?: string;
}
export class EmailService {
/**
* Send an email using AhaSend REST API
*/
async sendEmail(data: EmailData): Promise<boolean> {
try {
// Parse from email to separate email and name
const fromMatch = FROM_EMAIL.match(/^(.+?)\s*<(.+)>$/);
const fromEmail = fromMatch ? fromMatch[2] : FROM_EMAIL;
const fromName = fromMatch ? fromMatch[1].trim() : undefined;
const payload = {
from: {
email: fromEmail,
...(fromName && { name: fromName }),
},
recipients: [
{
email: data.to,
},
],
subject: data.subject,
text_content: data.text,
html_content: data.html || this.generateHtmlFromText(data.text),
};
console.log('[Email] Sending request to AhaSend API v2...');
console.log('[Email] API Key:', AHASEND_API_KEY.substring(0, 30) + '...');
console.log('[Email] Endpoint:', `/v2/accounts/${AHASEND_ACCOUNT_ID}/messages`);
const response = await axios.post(
`https://api.ahasend.com/v2/accounts/${AHASEND_ACCOUNT_ID}/messages`,
payload,
{
headers: {
'Authorization': `Bearer ${AHASEND_API_KEY}`,
'Content-Type': 'application/json',
},
}
);
console.log(`[Email] Successfully sent to ${data.to}:`, response.data);
return true;
} catch (error: any) {
console.error(`[Email] Failed to send to ${data.to}:`, error.response?.data || error.message);
if (error.response) {
console.error('[Email] Response status:', error.response.status);
console.error('[Email] Response headers:', error.response.headers);
}
return false;
}
}
/**
* Send notification email
*/
async sendNotificationEmail(
to: string,
firstName: string | null,
title: string,
message: string,
relatedType?: 'report' | 'event',
relatedId?: string
): Promise<boolean> {
const greeting = firstName ? `Hi ${firstName}` : 'Hi';
const text = `
${greeting},
${title}
${message}
${relatedType && relatedId ? `View details: ${this.getDetailsUrl(relatedType, relatedId)}` : ''}
---
Beach Litter Mapping Platform
To manage your notification preferences, visit your profile settings.
`.trim();
const html = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #0ea5e9; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; background-color: #f9fafb; }
.message { background-color: white; padding: 15px; border-radius: 5px; margin: 15px 0; }
.button { display: inline-block; padding: 12px 24px; background-color: #0ea5e9; color: white; text-decoration: none; border-radius: 5px; margin: 15px 0; }
.footer { text-align: center; padding: 20px; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🏖️ Beach Litter Platform</h1>
</div>
<div class="content">
<p>${greeting},</p>
<div class="message">
<h2>${title}</h2>
<p>${message}</p>
</div>
${relatedType && relatedId ? `
<a href="${this.getDetailsUrl(relatedType, relatedId)}" class="button">View Details</a>
` : ''}
</div>
<div class="footer">
<p>Beach Litter Mapping Platform</p>
<p>To manage your notification preferences, visit your profile settings.</p>
</div>
</div>
</body>
</html>
`.trim();
return this.sendEmail({
to,
subject: title,
text,
html,
});
}
/**
* Generate basic HTML from plain text
*/
private generateHtmlFromText(text: string): string {
return `
<!DOCTYPE html>
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
${text.split('\n').map(line => `<p>${line}</p>`).join('')}
</div>
</body>
</html>
`.trim();
}
/**
* Get URL for viewing details
*/
private getDetailsUrl(type: 'report' | 'event', id: string): string {
const baseUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
if (type === 'report') {
return `${baseUrl}/map?reportId=${id}`;
} else {
return `${baseUrl}/events?eventId=${id}`;
}
}
/**
* Test email configuration
*/
async testConnection(): Promise<boolean> {
try {
// Send a test email to verify configuration
const testEmail = process.env.TEST_EMAIL || 'test@example.com';
const success = await this.sendEmail({
to: testEmail,
subject: 'Beach Litter Platform - Email Test',
text: 'This is a test email from the Beach Litter Mapping Platform. If you received this, email notifications are working correctly!',
});
return success;
} catch (error: any) {
console.error('[Email] Connection test failed:', error.message);
return false;
}
}
}
export const emailService = new EmailService();