From 21d4c5f5793545108c86174cbd6e854e3b0e01d5 Mon Sep 17 00:00:00 2001 From: v0 Date: Fri, 20 Mar 2026 00:45:35 +0000 Subject: [PATCH] feat: implement contact form and Chatwoot widget Add Resend API-based contact form and Chatwoot widget integration. Co-authored-by: ASOwnerYT <23545044+ASOwnerYT@users.noreply.github.com> --- app/api/send-email/route.ts | 86 +++++++++++++++++++++++++++++++++++++ app/layout.tsx | 2 + components/chatwoot.tsx | 42 ++++++++++++++++++ components/contact.tsx | 49 ++++++++++++++++----- 4 files changed, 168 insertions(+), 11 deletions(-) create mode 100644 app/api/send-email/route.ts create mode 100644 components/chatwoot.tsx diff --git a/app/api/send-email/route.ts b/app/api/send-email/route.ts new file mode 100644 index 0000000..a918915 --- /dev/null +++ b/app/api/send-email/route.ts @@ -0,0 +1,86 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + try { + const { name, email, message } = await request.json(); + + // Validate inputs + if (!name || !email || !message) { + return NextResponse.json( + { error: 'Missing required fields' }, + { status: 400 } + ); + } + + // Validate email format + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return NextResponse.json( + { error: 'Invalid email address' }, + { status: 400 } + ); + } + + const resendApiKey = process.env.RESEND_API_KEY; + if (!resendApiKey) { + console.error('[v0] RESEND_API_KEY not configured'); + return NextResponse.json( + { error: 'Email service not configured' }, + { status: 500 } + ); + } + + // Send email via Resend + const response = await fetch('https://api.resend.com/emails', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${resendApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + from: 'Cloudrite Contact Form ', + to: 'contact@cloudrite.co.nz', + replyTo: email, + subject: `New Contact Form Submission from ${name}`, + html: ` +

New Contact Form Submission

+

Name: ${escapeHtml(name)}

+

Email: ${escapeHtml(email)}

+

Message:

+

${escapeHtml(message).replace(/\n/g, '
')}

+ `, + }), + }); + + if (!response.ok) { + const error = await response.json(); + console.error('[v0] Resend API error:', error); + return NextResponse.json( + { error: 'Failed to send email' }, + { status: 500 } + ); + } + + return NextResponse.json( + { success: true, message: 'Email sent successfully' }, + { status: 200 } + ); + } catch (error) { + console.error('[v0] Email route error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +function escapeHtml(text: string): string { + const map: { [key: string]: string } = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + return text.replace(/[&<>"']/g, (char) => map[char]); +} diff --git a/app/layout.tsx b/app/layout.tsx index 2e28e74..5e7eae2 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import { Space_Grotesk, Inter } from 'next/font/google' import { Analytics } from '@vercel/analytics/next' +import { ChatwootWidget } from '@/components/chatwoot' import './globals.css' const spaceGrotesk = Space_Grotesk({ @@ -45,6 +46,7 @@ export default function RootLayout({ {children} + ) diff --git a/components/chatwoot.tsx b/components/chatwoot.tsx new file mode 100644 index 0000000..e43b6d3 --- /dev/null +++ b/components/chatwoot.tsx @@ -0,0 +1,42 @@ +'use client' + +import { useEffect } from 'react' + +export function ChatwootWidget() { + useEffect(() => { + const chatwootToken = process.env.NEXT_PUBLIC_CHATWOOT_ACCOUNT_TOKEN + + if (!chatwootToken) { + console.warn('[v0] Chatwoot account token not configured') + return + } + + // Load Chatwoot widget script + const script = document.createElement('script') + script.src = 'https://app.chatwoot.com/packs/js/sdk.js' + script.async = true + + script.onload = () => { + // Initialize Chatwoot after script loads + if ((window as any).chatwootSDK) { + ;(window as any).chatwootSDK.run({ + websiteToken: chatwootToken, + baseUrl: 'https://app.chatwoot.com' + }) + } + } + + script.onerror = () => { + console.error('[v0] Failed to load Chatwoot widget') + } + + document.body.appendChild(script) + + return () => { + // Cleanup is handled by Chatwoot itself + // Don't remove the script as it manages its own lifecycle + } + }, []) + + return null +} diff --git a/components/contact.tsx b/components/contact.tsx index 2d2a01e..966f768 100644 --- a/components/contact.tsx +++ b/components/contact.tsx @@ -3,7 +3,7 @@ import { useState } from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" -import { Mail, Phone, MapPin, Send, ArrowRight } from "lucide-react" +import { Mail, Phone, MapPin, Send, ArrowRight, AlertCircle } from "lucide-react" export function Contact() { const [formData, setFormData] = useState({ @@ -13,20 +13,39 @@ export function Contact() { }) const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitted, setIsSubmitted] = useState(false) + const [error, setError] = useState("") const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setIsSubmitting(true) + setError("") - // Simulate form submission - await new Promise(resolve => setTimeout(resolve, 1500)) - - setIsSubmitting(false) - setIsSubmitted(true) - setFormData({ name: "", email: "", message: "" }) - - // Reset success message after 5 seconds - setTimeout(() => setIsSubmitted(false), 5000) + try { + const response = await fetch('/api/send-email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData), + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'Failed to send message') + } + + setIsSubmitted(true) + setFormData({ name: "", email: "", message: "" }) + + // Reset success message after 5 seconds + setTimeout(() => setIsSubmitted(false), 5000) + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred' + setError(errorMessage) + console.error('[v0] Form submission error:', err) + } finally { + setIsSubmitting(false) + } } const contactInfo = [ @@ -129,7 +148,14 @@ export function Contact() {

) : ( -
+ <> + {error && ( +
+ +

{error}

+
+ )} +