UNPKG

freedback

Version:

A free, self-hosted feedback widget for Next.js apps with multiple storage options and AI-powered insights

174 lines (150 loc) 5.89 kB
import { NextApiRequest, NextApiResponse } from 'next'; import { createClient } from '@supabase/supabase-js'; import { Resend } from 'resend'; // Initialize Supabase client with anon key (optional - only if credentials are provided) // Email template utility (bundled) // Metadata interface matching the frontend interface Metadata { browser: { userAgent: string; language: string; platform: string; viewport: { width: number; height: number; }; }; context: { url: string; timestamp: string; referrer: string; location?: { city: string; country: string; timezone: string; continent: string; }; }; } // Email template utility (bundled) interface EmailTemplateData { content: string; email?: string; emoji?: string; metadata?: Metadata; } function generateEmailTemplate(data: EmailTemplateData): string { const { content, email, emoji, metadata } = data; return ` <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;"> <h2 style="color: #374151; margin-bottom: 24px;">New Feedback Received</h2> <div style="background: #f8fafc; padding: 16px; border-radius: 8px; margin-bottom: 20px;"> <div style="font-size: 18px; font-weight: 600; margin-bottom: 8px;"> ${emoji || '💬'} ${content} </div> ${email ? `<div style="color: #64748b; font-size: 14px;">From: ${email}</div>` : ''} </div> <div style="border-top: 1px solid #e2e8f0; padding-top: 16px;"> <h3 style="color: #374151; font-size: 14px; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.05em;">Context</h3> <div style="display: grid; gap: 8px; font-size: 14px;"> <div> <span style="color: #6b7280;">🕒 </span> <span>${new Date().toLocaleString()}</span> </div> <div> <span style="color: #6b7280;">🔗 </span> <a href="${metadata?.context?.url || 'Unknown'}" style="color: #374151; text-decoration: none;">${metadata?.context?.url || 'Unknown'}</a> </div> ${metadata?.context?.location ? ` <div> <span style="color: #6b7280;">🌍 </span> <span>${metadata.context.location.city}, ${metadata.context.location.country} (${metadata.context.location.timezone})</span> </div> ` : ''} ${metadata?.browser?.platform ? ` <div> <span style="color: #6b7280;">🖥️ </span> <span>${metadata.browser.platform}</span> </div> ` : ''} ${metadata?.browser?.language ? ` <div> <span style="color: #6b7280;">🌐 </span> <span>${metadata.browser.language}</span> </div> ` : ''} ${metadata?.browser?.viewport ? ` <div> <span style="color: #6b7280;">📱 </span> <span>${metadata.browser.viewport.width}×${metadata.browser.viewport.height}</span> </div> ` : ''} </div> </div> <div style="margin-top: 24px; padding-top: 16px; border-top: 1px solid #e2e8f0; font-size: 12px; color: #9ca3af; text-align: center;"> Powered by <a href="https://freedback.dev" style="color: #374151; text-decoration: none;">Freedback</a> </div> </div> `; } const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; const supabase = (supabaseUrl && supabaseAnonKey) ? createClient(supabaseUrl, supabaseAnonKey) : null; // Initialize Resend (optional - only if API key is provided) const resendApiKey = process.env.RESEND_API_KEY; const notificationEmail = process.env.FREEDBACK_EMAIL_NOTIFICATION; const fromEmail = process.env.FREEDBACK_EMAIL_FROM; const resend = resendApiKey ? new Resend(resendApiKey) : null; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } try { const { content, email, emoji, metadata } = req.body; if (!content || content.trim().length === 0) { return res.status(400).json({ error: 'Content is required' }); } // Insert feedback into Supabase (if configured) if (supabase) { const { error } = await supabase .from('freedback') .insert([ { content: content.trim(), email: email || null, emoji: emoji || null, metadata: metadata || null, }, ]); if (error) { console.error('Supabase error:', error); return res.status(500).json({ error: 'Failed to save feedback' }); } } // Send email notification if Resend is configured if (resend && notificationEmail && fromEmail) { try { await resend.emails.send({ from: fromEmail, to: notificationEmail, subject: `New Feedback Received ${emoji || ''}`, html: generateEmailTemplate({ content, email, emoji, metadata }), }); } catch (emailError) { console.error('Email notification failed:', emailError); // Don't fail the request if email fails } } // For email-only mode, ensure at least one action was taken if (!supabase && (!resend || !notificationEmail || !fromEmail)) { console.log('Feedback received (console-only mode):', { content, email, emoji, metadata }); } return res.status(200).json({ success: true }); } catch (error) { console.error('API error:', error); return res.status(500).json({ error: 'Internal server error' }); } }