@api-buddy/sendgrid
Version:
API Buddy integration for SendGrid - Email delivery service for transactional and marketing emails
466 lines (390 loc) • 12.8 kB
JavaScript
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const readline = require('readline');
const chalk = require('chalk');
const { promisify } = require('util');
const exec = promisify(require('child_process').exec);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const log = console.log;
const error = (message) => console.error(chalk.red(`❌ ${message}`));
const success = (message) => log(chalk.green(`✅ ${message}`));
const info = (message) => log(chalk.blue(`ℹ️ ${message}`));
// Check if running in a Next.js project
function isNextJsProject() {
try {
const packageJson = require(path.join(process.cwd(), 'package.json'));
return packageJson.dependencies?.next || packageJson.devDependencies?.next;
} catch (e) {
return false;
}
}
// Create directory if it doesn't exist
function ensureDirectoryExists(directory) {
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
}
// Install required dependencies
async function installDependencies() {
info('Installing required dependencies...');
const dependencies = [
'@sendgrid/mail@^8.0.0',
];
try {
// Check if using npm, yarn, or pnpm
const packageManager = fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml'))
? 'pnpm add'
: fs.existsSync(path.join(process.cwd(), 'yarn.lock'))
? 'yarn add'
: 'npm install';
// Install dependencies
await exec(`${packageManager} ${dependencies.join(' ')}`);
return true;
} catch (err) {
error('Failed to install dependencies. Please install them manually:');
error(` ${packageManager} ${dependencies.join(' ')}`);
return false;
}
}
// Create API route for sending emails
function createApiRoute() {
const apiDir = path.join(process.cwd(), 'app', 'api');
const emailDir = path.join(apiDir, 'email');
ensureDirectoryExists(emailDir);
// Create route.ts
const routeContent = `import { NextResponse } from 'next/server';
import { sendEmail } from '@api-buddy/sendgrid';
import { EmailOptions } from '@api-buddy/sendgrid/types';
// Define the expected request body type
interface SendEmailRequest {
to: string | string[];
subject: string;
text?: string;
html?: string;
templateId?: string;
dynamicTemplateData?: Record<string, any>;
}
export async function POST(req: Request) {
try {
const body = (await req.json()) as SendEmailRequest;
// Validate required fields
if (!body.to) {
return NextResponse.json(
{ error: 'Recipient email is required' },
{ status: 400 }
);
}
if (!body.subject) {
return NextResponse.json(
{ error: 'Email subject is required' },
{ status: 400 }
);
}
if (!body.text && !body.html && !body.templateId) {
return NextResponse.json(
{ error: 'Email content (text, html, or templateId) is required' },
{ status: 400 }
);
}
// Prepare email options
const emailOptions: EmailOptions = {
to: body.to,
from: process.env.SENDGRID_FROM_EMAIL || 'noreply@example.com',
subject: body.subject,
};
if (body.text) emailOptions.text = body.text;
if (body.html) emailOptions.html = body.html;
if (body.templateId) emailOptions.templateId = body.templateId;
if (body.dynamicTemplateData) emailOptions.dynamicTemplateData = body.dynamicTemplateData;
// Send email
const result = await sendEmail(emailOptions);
return NextResponse.json({
success: true,
message: 'Email sent successfully',
data: result,
});
} catch (error) {
console.error('Error sending email:', error);
return NextResponse.json(
{
success: false,
error: 'Failed to send email',
details: error.message
},
{ status: 500 }
);
}
}
// Add OPTIONS method for CORS preflight
// This is necessary for API routes that receive POST requests from the browser
export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
`;
fs.writeFileSync(
path.join(emailDir, 'route.ts'),
routeContent
);
return true;
}
// Create a React hook for sending emails
function createEmailHook() {
const hooksDir = path.join(process.cwd(), 'src', 'hooks');
ensureDirectoryExists(hooksDir);
const hookContent = `'use client';
import { useState } from 'react';
interface SendEmailOptions {
to: string | string[];
subject: string;
text?: string;
html?: string;
templateId?: string;
dynamicTemplateData?: Record<string, any>;
}
interface UseEmailResult {
sendEmail: (options: SendEmailOptions) => Promise<any>;
isLoading: boolean;
error: Error | null;
reset: () => void;
}
export function useEmail(): UseEmailResult {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const sendEmail = async (options: SendEmailOptions) => {
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(options),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to send email');
}
return await response.json();
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to send email'));
throw err;
} finally {
setIsLoading(false);
}
};
const reset = () => {
setError(null);
};
return {
sendEmail,
isLoading,
error,
reset,
};
}
`;
fs.writeFileSync(
path.join(hooksDir, 'useEmail.ts'),
hookContent
);
return true;
}
// Update environment variables
function updateEnvFile() {
const envPath = path.join(process.cwd(), '.env.local');
let envContent = '';
if (fs.existsSync(envPath)) {
envContent = fs.readFileSync(envPath, 'utf-8');
}
// Add required environment variables if they don't exist
if (!envContent.includes('SENDGRID_API_KEY')) {
envContent += '\n# SendGrid API Key\nSENDGRID_API_KEY=your_sendgrid_api_key_here\n';
}
if (!envContent.includes('SENDGRID_FROM_EMAIL')) {
envContent += '\n# Default "from" email address\nSENDGRID_FROM_EMAIL=your-email@example.com\n';
}
fs.writeFileSync(envPath, envContent.trim() + '\n');
return true;
}
// Create example component
function createExampleComponent() {
const componentsDir = path.join(process.cwd(), 'src', 'components');
ensureDirectoryExists(componentsDir);
const componentContent = `'use client';
import { useState } from 'react';
import { useEmail } from '../hooks/useEmail';
export function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
});
const { sendEmail, isLoading, error, reset } = useEmail();
const [isSubmitted, setIsSubmitted] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await sendEmail({
to: process.env.NEXT_PUBLIC_CONTACT_EMAIL || 'contact@example.com',
subject: \`New Contact Form Submission from \${formData.name}\`,
html: \`
<h1>New Contact Form Submission</h1>
<p><strong>Name:</strong> \${formData.name}</p>
<p><strong>Email:</strong> \${formData.email}</p>
<p><strong>Message:</strong></p>
<p>\${formData.message.replace(/\n/g, '<br>')}</p>
\`,
text: \`
New Contact Form Submission\n\n
Name: \${formData.name}\n
Email: \${formData.email}\n\n
Message:\n\${formData.message}
\`,
});
setIsSubmitted(true);
setFormData({ name: '', email: '', message: '' });
// Reset form after 5 seconds
setTimeout(() => {
setIsSubmitted(false);
}, 5000);
} catch (err) {
console.error('Failed to send message:', err);
}
};
if (isSubmitted) {
return (
<div className="p-6 bg-green-50 text-green-800 rounded-lg">
<h3 className="font-bold text-lg mb-2">Thank you!</h3>
<p>Your message has been sent successfully. We'll get back to you soon!</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="p-4 bg-red-50 text-red-700 rounded-lg mb-4">
{error.message}
</div>
)}
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
Name
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1">
Message
</label>
<textarea
id="message"
name="message"
rows={4}
value={formData.message}
onChange={handleChange}
required
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<button
type="submit"
disabled={isLoading}
className={\`
px-6 py-2 bg-blue-600 text-white font-medium rounded-md
hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500
\${isLoading ? 'opacity-70 cursor-not-allowed' : ''}
\`}
>
{isLoading ? 'Sending...' : 'Send Message'}
</button>
</div>
</form>
);
}
`;
fs.writeFileSync(
path.join(componentsDir, 'ContactForm.tsx'),
componentContent
);
return true;
}
// Main function
async function main() {
try {
info('🚀 Setting up SendGrid with API Buddy...');
// Check if this is a Next.js project
if (!isNextJsProject()) {
error('This does not appear to be a Next.js project. Please run this command in a Next.js project directory.');
process.exit(1);
}
// Install dependencies
const depsInstalled = await installDependencies();
if (!depsInstalled) {
process.exit(1);
}
// Create API route
createApiRoute();
// Create React hook
createEmailHook();
// Create example component
createExampleComponent();
// Update environment variables
updateEnvFile();
success('✅ SendGrid setup completed successfully!');
info('\nNext steps:');
info('1. Update your .env.local file with your SendGrid API key and email');
info('2. Import and use the ContactForm component in your pages');
info('3. Start your development server with: npm run dev');
info('\nDocumentation: https://docs.sendgrid.com/');
} catch (err) {
error(\`Failed to set up SendGrid: \${err.message}\`);
process.exit(1);
} finally {
rl.close();
}
}
// Run the setup
main();