feat(contact): add contact form and server-side email via SMTP (Office365); .env.example updated
This commit is contained in:
12
.env.example
12
.env.example
@@ -9,4 +9,14 @@ DB_NAME=your-db-name
|
||||
|
||||
# Admin credentials
|
||||
ADMIN_USER=your-admin-username
|
||||
ADMIN_PASSWORD=your-admin-password
|
||||
ADMIN_PASSWORD=your-admin-password
|
||||
|
||||
# SMTP (Office365) settings for sending contact/booking emails
|
||||
# Use the Office365 SMTP endpoint and a mailbox account that can send (e.g. christian@warme.dk)
|
||||
SMTP_HOST=smtp.office365.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-smtp-user@example.com
|
||||
SMTP_PASS=your-smtp-password
|
||||
|
||||
# The recipient for booking messages (defaults to christian@warme.dk)
|
||||
BOOKING_EMAIL=christian@warme.dk
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"lucide-react": "^0.292.0",
|
||||
"mysql2": "^3.15.3",
|
||||
"next": "^14.2.33",
|
||||
"nodemailer": "^7.0.10",
|
||||
"openai": "^6.7.0",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.9.1",
|
||||
@@ -4323,6 +4324,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "7.0.10",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz",
|
||||
"integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"lucide-react": "^0.292.0",
|
||||
"mysql2": "^3.15.3",
|
||||
"next": "^14.2.33",
|
||||
"nodemailer": "^7.0.10",
|
||||
"openai": "^6.7.0",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.9.1",
|
||||
|
||||
64
src/app/api/contact/route.ts
Normal file
64
src/app/api/contact/route.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import nodemailer from 'nodemailer'
|
||||
|
||||
// Simple validation helper
|
||||
function validate(body: any) {
|
||||
if (!body) return 'No data'
|
||||
const { name, email, phone, message } = body
|
||||
if (!name || !email || !message) return 'Missing required fields'
|
||||
return null
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const err = validate(body)
|
||||
if (err) return NextResponse.json({ error: err }, { status: 400 })
|
||||
|
||||
// Read SMTP config from environment
|
||||
const host = process.env.SMTP_HOST || 'smtp.office365.com'
|
||||
const port = parseInt(process.env.SMTP_PORT || '587', 10)
|
||||
const user = process.env.SMTP_USER
|
||||
const pass = process.env.SMTP_PASS
|
||||
const to = process.env.BOOKING_EMAIL || 'christian@warme.dk'
|
||||
|
||||
if (!user || !pass) {
|
||||
console.error('SMTP credentials not configured')
|
||||
return NextResponse.json({ error: 'Email service not configured' }, { status: 500 })
|
||||
}
|
||||
|
||||
// Create transporter
|
||||
const transporter = nodemailer.createTransport({
|
||||
host,
|
||||
port,
|
||||
secure: port === 465, // true for 465, false for other ports
|
||||
auth: {
|
||||
user,
|
||||
pass
|
||||
}
|
||||
})
|
||||
|
||||
const { name, email, phone, message } = body
|
||||
|
||||
const subject = `Kontaktformular: ${name}`
|
||||
const html = `
|
||||
<p><strong>Navn:</strong> ${name}</p>
|
||||
<p><strong>Email:</strong> ${email}</p>
|
||||
<p><strong>Telefon:</strong> ${phone || '-'} </p>
|
||||
<p><strong>Besked:</strong></p>
|
||||
<div>${message.replace(/\n/g, '<br/>')}</div>
|
||||
`
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `${name} <${email}>`,
|
||||
to,
|
||||
subject,
|
||||
html
|
||||
})
|
||||
|
||||
return NextResponse.json({ ok: true })
|
||||
} catch (error) {
|
||||
console.error('Error in contact POST:', error)
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { sampleData } from '@/config/sample-data'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Mail, Phone, Instagram, Facebook } from 'lucide-react'
|
||||
import ContactForm from '@/components/contact-form'
|
||||
|
||||
export default function ContactPage() {
|
||||
const { contact } = sampleData.chef
|
||||
@@ -53,41 +53,7 @@ export default function ContactPage() {
|
||||
{/* Contact Form */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-6">Send besked</h2>
|
||||
<form className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Navn
|
||||
</label>
|
||||
<Input type="text" className="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Email
|
||||
</label>
|
||||
<Input type="email" className="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Telefon
|
||||
</label>
|
||||
<Input type="tel" className="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Besked
|
||||
</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
className="w-full rounded-md border border-primary-200 bg-white px-3 py-2 text-sm"
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-accent hover:bg-accent-600 text-white font-medium py-2 px-4 rounded-md transition-colors"
|
||||
>
|
||||
Send besked
|
||||
</button>
|
||||
</form>
|
||||
<ContactForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
86
src/components/contact-form.tsx
Normal file
86
src/components/contact-form.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
export default function ContactForm() {
|
||||
const [name, setName] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [phone, setPhone] = useState('')
|
||||
const [message, setMessage] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [success, setSuccess] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setSuccess(null)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, email, phone, message })
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data?.error || 'Unknown error')
|
||||
setSuccess('Besked sendt — tak!')
|
||||
setName('')
|
||||
setEmail('')
|
||||
setPhone('')
|
||||
setMessage('')
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Der skete en fejl')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{success && (
|
||||
<div className="p-3 bg-green-100 text-green-800 rounded">{success}</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="p-3 bg-red-100 text-red-800 rounded">{error}</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Navn</label>
|
||||
<Input type="text" value={name} onChange={e => setName(e.target.value)} className="w-full" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Email</label>
|
||||
<Input type="email" value={email} onChange={e => setEmail(e.target.value)} className="w-full" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Telefon</label>
|
||||
<Input type="tel" value={phone} onChange={e => setPhone(e.target.value)} className="w-full" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Besked</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
value={message}
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
className="w-full rounded-md border border-primary-200 bg-white px-3 py-2 text-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-accent hover:bg-accent-600 text-white font-medium py-2 px-4 rounded-md transition-colors"
|
||||
>
|
||||
{loading ? 'Sender...' : 'Send besked'}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user