UNPKG

@api-buddy/sendgrid

Version:

API Buddy integration for SendGrid - Email delivery service for transactional and marketing emails

466 lines (390 loc) 12.8 kB
#!/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();