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.

Your message:

${message}

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}`) })