feat: add resend for email

fix: nebual minified react error
This commit is contained in:
jwinterm
2026-01-29 11:23:54 -05:00
parent b80723cd28
commit 2838133681
5 changed files with 95 additions and 50 deletions
+6 -2
View File
@@ -3,5 +3,9 @@
NEXT_PUBLIC_TURNSTILE_SITE_KEY=your_site_key_here
TURNSTILE_SECRET_KEY=your_secret_key_here
# Optional: Email for contact form notifications
# CONTACT_EMAIL=your@email.com
# Resend (email service for contact form)
# Get API key from: https://resend.com/api-keys
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Email address to receive contact form submissions
CONTACT_EMAIL=sales@such.software
+1
View File
@@ -22,6 +22,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.5.0",
"resend": "^4.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
+30 -10
View File
@@ -1,7 +1,10 @@
'use server';
import { z } from 'zod';
import { verifyTurnstile } from '@/lib/turnstile'; // Helper to call Cloudflare API
import { Resend } from 'resend';
import { verifyTurnstile } from '@/lib/turnstile';
const resend = new Resend(process.env.RESEND_API_KEY);
const ContactSchema = z.object({
name: z.string().min(2, "Name is too short"),
@@ -12,7 +15,7 @@ const ContactSchema = z.object({
export async function submitContactForm(prevState: any, formData: FormData) {
const rawData = Object.fromEntries(formData.entries());
// 1. Zod Validation
const validated = ContactSchema.safeParse(rawData);
if (!validated.success) {
@@ -25,17 +28,34 @@ export async function submitContactForm(prevState: any, formData: FormData) {
return { success: false, message: "Security check failed. Please refresh." };
}
// 3. Send to Backend (or Email)
// 3. Send email via Resend
try {
// TODO: Connect to email service or CMS
console.log("Form submitted:", validated.data);
// Simulating success for now
await new Promise(resolve => setTimeout(resolve, 500));
const { name, email, message } = validated.data;
const contactEmail = process.env.CONTACT_EMAIL || 'sales@such.software';
const { error } = await resend.emails.send({
from: 'Such Software <noreply@such.software>',
to: contactEmail,
replyTo: email,
subject: `New inquiry from ${name}`,
text: `Name: ${name}\nEmail: ${email}\n\nMessage:\n${message}`,
html: `
<h2>New Project Inquiry</h2>
<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> <a href="mailto:${email}">${email}</a></p>
<h3>Message:</h3>
<p>${message.replace(/\n/g, '<br>')}</p>
`,
});
if (error) {
console.error('Resend error:', error);
return { success: false, message: "Failed to send message. Please try again." };
}
return { success: true, message: "Request received. We will be in touch shortly." };
} catch (e) {
console.error(e);
console.error('Email error:', e);
return { success: false, message: "System error. Please contact us via email." };
}
}
@@ -1,18 +1,41 @@
'use client';
import { Turnstile } from '@marsidev/react-turnstile';
import { useState, useEffect } from 'react';
const SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
export function TurnstileWidget({ onVerify }: { onVerify: (token: string) => void }) {
const [mounted, setMounted] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!SITE_KEY) {
console.warn('NEXT_PUBLIC_TURNSTILE_SITE_KEY is not set');
return null;
}
// Only render on client after mount to avoid hydration issues
if (!mounted) {
return <div className="h-[65px] w-[300px]" />;
}
if (error) {
return (
<div className="text-xs text-muted-foreground">
Security check unavailable
</div>
);
}
return (
<Turnstile
siteKey={SITE_KEY}
onSuccess={onVerify}
onError={() => setError(true)}
onExpire={() => onVerify('')}
options={{ theme: 'auto' }}
/>
);