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 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 · NHS Norfolk & Waveney ICB


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