Merge pull request #2 from ASOwnerYT/v0/asowneryt-0dbbb987
Update landing page metrics and add contact features
This commit was merged in pull request #2.
This commit is contained in:
@@ -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 <[email protected]>',
|
||||||
|
to: '[email protected]',
|
||||||
|
replyTo: email,
|
||||||
|
subject: `New Contact Form Submission from ${name}`,
|
||||||
|
html: `
|
||||||
|
<h2>New Contact Form Submission</h2>
|
||||||
|
<p><strong>Name:</strong> ${escapeHtml(name)}</p>
|
||||||
|
<p><strong>Email:</strong> ${escapeHtml(email)}</p>
|
||||||
|
<p><strong>Message:</strong></p>
|
||||||
|
<p>${escapeHtml(message).replace(/\n/g, '<br>')}</p>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { Space_Grotesk, Inter } from 'next/font/google'
|
import { Space_Grotesk, Inter } from 'next/font/google'
|
||||||
import { Analytics } from '@vercel/analytics/next'
|
import { Analytics } from '@vercel/analytics/next'
|
||||||
|
import { ChatwootWidget } from '@/components/chatwoot'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
|
|
||||||
const spaceGrotesk = Space_Grotesk({
|
const spaceGrotesk = Space_Grotesk({
|
||||||
@@ -45,6 +46,7 @@ export default function RootLayout({
|
|||||||
<body className={`${spaceGrotesk.variable} ${inter.variable} font-sans antialiased`}>
|
<body className={`${spaceGrotesk.variable} ${inter.variable} font-sans antialiased`}>
|
||||||
{children}
|
{children}
|
||||||
<Analytics />
|
<Analytics />
|
||||||
|
<ChatwootWidget />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+96
-69
@@ -3,7 +3,7 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
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() {
|
export function Contact() {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
@@ -13,20 +13,39 @@ export function Contact() {
|
|||||||
})
|
})
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [isSubmitted, setIsSubmitted] = useState(false)
|
const [isSubmitted, setIsSubmitted] = useState(false)
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
setError("")
|
||||||
|
|
||||||
// Simulate form submission
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
const response = await fetch('/api/send-email', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
})
|
||||||
|
|
||||||
setIsSubmitting(false)
|
if (!response.ok) {
|
||||||
setIsSubmitted(true)
|
const errorData = await response.json()
|
||||||
setFormData({ name: "", email: "", message: "" })
|
throw new Error(errorData.error || 'Failed to send message')
|
||||||
|
}
|
||||||
|
|
||||||
// Reset success message after 5 seconds
|
setIsSubmitted(true)
|
||||||
setTimeout(() => setIsSubmitted(false), 5000)
|
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 = [
|
const contactInfo = [
|
||||||
@@ -129,70 +148,78 @@ export function Contact() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<>
|
||||||
<div className="space-y-2">
|
{error && (
|
||||||
<label htmlFor="name" className="text-sm font-medium">
|
<div className="mb-6 p-4 bg-destructive/10 border border-destructive/50 rounded-lg flex items-center gap-3">
|
||||||
Your Name
|
<AlertCircle className="w-5 h-5 text-destructive shrink-0" />
|
||||||
</label>
|
<p className="text-sm text-destructive">{error}</p>
|
||||||
<Input
|
</div>
|
||||||
id="name"
|
)}
|
||||||
type="text"
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
placeholder="John Smith"
|
<div className="space-y-2">
|
||||||
value={formData.name}
|
<label htmlFor="name" className="text-sm font-medium">
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
Your Name
|
||||||
required
|
</label>
|
||||||
className="bg-background border-border focus:border-primary transition-colors"
|
<Input
|
||||||
/>
|
id="name"
|
||||||
</div>
|
type="text"
|
||||||
|
placeholder="John Smith"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
required
|
||||||
|
className="bg-background border-border focus:border-primary transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="email" className="text-sm font-medium">
|
<label htmlFor="email" className="text-sm font-medium">
|
||||||
Email Address
|
Email Address
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="[email protected]"
|
placeholder="[email protected]"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
required
|
required
|
||||||
className="bg-background border-border focus:border-primary transition-colors"
|
className="bg-background border-border focus:border-primary transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="message" className="text-sm font-medium">
|
<label htmlFor="message" className="text-sm font-medium">
|
||||||
How Can We Help?
|
How Can We Help?
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="message"
|
id="message"
|
||||||
placeholder="Tell us about your project or what you need help with..."
|
placeholder="Tell us about your project or what you need help with..."
|
||||||
value={formData.message}
|
value={formData.message}
|
||||||
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
||||||
required
|
required
|
||||||
rows={5}
|
rows={5}
|
||||||
className="w-full px-4 py-3 bg-background border border-border rounded-lg resize-none focus:outline-none focus:border-primary transition-colors"
|
className="w-full px-4 py-3 bg-background border border-border rounded-lg resize-none focus:outline-none focus:border-primary transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="w-full bg-primary text-primary-foreground hover:bg-primary/90 py-6 text-lg transition-all duration-300 hover:scale-[1.02] hover:shadow-lg hover:shadow-primary/25 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
|
className="w-full bg-primary text-primary-foreground hover:bg-primary/90 py-6 text-lg transition-all duration-300 hover:scale-[1.02] hover:shadow-lg hover:shadow-primary/25 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<span className="w-5 h-5 border-2 border-primary-foreground/30 border-t-primary-foreground rounded-full animate-spin" />
|
<span className="w-5 h-5 border-2 border-primary-foreground/30 border-t-primary-foreground rounded-full animate-spin" />
|
||||||
Sending...
|
Sending...
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
Send Message
|
Send Message
|
||||||
<Send className="w-5 h-5" />
|
<Send className="w-5 h-5" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user