import 'dotenv/config'
import express from 'express'
import nodemailer from 'nodemailer'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const app = express()
app.use(express.json())
// Serve static files from Vite build (dist/ is at project root, one level up from dist-server/)
app.use(express.static(path.join(__dirname, '..', 'dist')))
// Contact API endpoint
app.post('/api/contact', async (req, res) => {
const { name, organisation, email, subject, message } = req.body
if (!name || !email || !subject || !message) {
return res.status(400).json({ success: false, message: 'All fields are required' })
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
return res.status(400).json({ success: false, message: 'Invalid email address' })
}
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
})
const contactEmail = process.env.CONTACT_EMAIL || 'andy@charlwood.xyz'
try {
// Admin notification
await transporter.sendMail({
from: `"${name}" <${process.env.SMTP_USER}>`,
replyTo: email,
to: contactEmail,
subject: `Portfolio Referral: ${subject}`,
html: `
New Patient Referral
Referring Clinician: ${name}
Organisation: ${organisation || 'Not specified'}
Email: ${email}
Subject: ${subject}
Clinical Details:
${message}
This message was sent from your portfolio contact form.
`,
})
// Auto-reply
await transporter.sendMail({
from: `"Andy Charlwood" <${process.env.SMTP_USER}>`,
to: email,
subject: 'Thanks for getting in touch!',
html: `
Thanks for your message, ${name}!
I've received your referral and will get back to you as soon as possible.
Best regards,
Andy Charlwood
Informatics Pharmacist
This is an automated confirmation. Please do not reply to this email.
`,
})
return res.status(200).json({ success: true, message: 'Referral sent successfully!' })
} catch (error) {
console.error('Email error:', error)
return res.status(500).json({ success: false, message: 'Failed to send referral. Please try again.' })
}
})
// Chat proxy endpoint — keeps API key server-side
app.post('/api/chat', async (req, res) => {
const apiKey = process.env.OPEN_ROUTER_API_KEY
if (!apiKey) {
return res.status(500).json({ error: 'LLM API key not configured' })
}
try {
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
'HTTP-Referer': req.headers.origin || req.headers.referer || '',
'X-Title': 'Andy Charlwood Portfolio',
},
body: JSON.stringify(req.body),
})
if (!response.ok) {
return res.status(response.status).json({ error: `LLM API error: ${response.status}` })
}
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
const reader = response.body?.getReader()
if (!reader) {
return res.status(500).json({ error: 'No response body' })
}
const pump = async () => {
while (true) {
const { done, value } = await reader.read()
if (done) break
res.write(value)
}
res.end()
}
await pump()
} catch (error) {
console.error('Chat proxy error:', error)
if (!res.headersSent) {
return res.status(500).json({ error: 'Failed to proxy chat request' })
}
res.end()
}
})
// SPA fallback
app.get('*', (_req, res) => {
res.sendFile(path.join(__dirname, '..', 'dist', 'index.html'))
})
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})