diff --git a/Dockerfile b/Dockerfile index d4ef0ba..32f6f8c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,8 @@ -FROM nginx:alpine -COPY index.html /usr/share/nginx/html/ -COPY img/ /usr/share/nginx/html/img/ +FROM node:20-alpine +WORKDIR /app +COPY package.json ./ +RUN npm install --production +COPY server.js ./ +COPY public/ ./public/ EXPOSE 80 +CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml index 8fcb0b7..291974c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,14 @@ services: restart: unless-stopped networks: - traefik-public + - mailcow-network + environment: + - SMTP_HOST=mailcowdockerized-postfix-mailcow-1 + - SMTP_PORT=587 + - SMTP_USER=${SMTP_USER} + - SMTP_PASS=${SMTP_PASS} + - SMTP_FROM=${SMTP_FROM:-noreply@jeffemmett.com} + - CONTACT_TO=noire.michelle@gmail.com labels: - "traefik.enable=true" - "traefik.http.routers.cineasthesia-landing.rule=Host(`cineasthesia.com`) || Host(`www.cineasthesia.com`)" @@ -14,3 +22,6 @@ services: networks: traefik-public: external: true + mailcow-network: + external: true + name: mailcowdockerized_mailcow-network diff --git a/package.json b/package.json new file mode 100644 index 0000000..4e6a1a9 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "cineasthesia-landing", + "private": true, + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "nodemailer": "^6.9.0" + } +} diff --git a/img/acorus-garden.jpg b/public/img/acorus-garden.jpg similarity index 100% rename from img/acorus-garden.jpg rename to public/img/acorus-garden.jpg diff --git a/img/chroma-1.jpg b/public/img/chroma-1.jpg similarity index 100% rename from img/chroma-1.jpg rename to public/img/chroma-1.jpg diff --git a/img/chroma-2.jpg b/public/img/chroma-2.jpg similarity index 100% rename from img/chroma-2.jpg rename to public/img/chroma-2.jpg diff --git a/img/chroma-3.jpg b/public/img/chroma-3.jpg similarity index 100% rename from img/chroma-3.jpg rename to public/img/chroma-3.jpg diff --git a/img/elle-sam.jpg b/public/img/elle-sam.jpg similarity index 100% rename from img/elle-sam.jpg rename to public/img/elle-sam.jpg diff --git a/img/hero-landscape.jpg b/public/img/hero-landscape.jpg similarity index 100% rename from img/hero-landscape.jpg rename to public/img/hero-landscape.jpg diff --git a/img/sankofa-accra.jpg b/public/img/sankofa-accra.jpg similarity index 100% rename from img/sankofa-accra.jpg rename to public/img/sankofa-accra.jpg diff --git a/img/schwartz-butterfly.jpg b/public/img/schwartz-butterfly.jpg similarity index 100% rename from img/schwartz-butterfly.jpg rename to public/img/schwartz-butterfly.jpg diff --git a/img/schwartz-web-of-life.jpg b/public/img/schwartz-web-of-life.jpg similarity index 100% rename from img/schwartz-web-of-life.jpg rename to public/img/schwartz-web-of-life.jpg diff --git a/img/story-family.jpg b/public/img/story-family.jpg similarity index 100% rename from img/story-family.jpg rename to public/img/story-family.jpg diff --git a/img/worlding-family.jpg b/public/img/worlding-family.jpg similarity index 100% rename from img/worlding-family.jpg rename to public/img/worlding-family.jpg diff --git a/img/worlding-hero.jpg b/public/img/worlding-hero.jpg similarity index 100% rename from img/worlding-hero.jpg rename to public/img/worlding-hero.jpg diff --git a/img/worlding-landscape.jpg b/public/img/worlding-landscape.jpg similarity index 100% rename from img/worlding-landscape.jpg rename to public/img/worlding-landscape.jpg diff --git a/img/worlding-pillars.jpg b/public/img/worlding-pillars.jpg similarity index 100% rename from img/worlding-pillars.jpg rename to public/img/worlding-pillars.jpg diff --git a/index.html b/public/index.html similarity index 99% rename from index.html rename to public/index.html index 9da00d1..5078336 100644 --- a/index.html +++ b/public/index.html @@ -961,8 +961,8 @@ form.addEventListener('submit', async (e) => { throw new Error('Server error'); } } catch (err) { - status.className = 'form-status success'; - status.textContent = 'Thank you for your interest. Please email us directly at hello@cineasthesia.com'; + status.className = 'form-status error'; + status.textContent = 'Something went wrong. Please try again or email noire.michelle@gmail.com directly.'; } btn.textContent = 'Send Message'; diff --git a/server.js b/server.js new file mode 100644 index 0000000..8e60741 --- /dev/null +++ b/server.js @@ -0,0 +1,120 @@ +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const nodemailer = require('nodemailer'); + +const PORT = 80; +const STATIC_DIR = path.join(__dirname, 'public'); + +const SMTP_HOST = process.env.SMTP_HOST || 'mail.rmail.online'; +const SMTP_PORT = parseInt(process.env.SMTP_PORT || '587'); +const SMTP_USER = process.env.SMTP_USER; +const SMTP_PASS = process.env.SMTP_PASS; +const SMTP_FROM = process.env.SMTP_FROM || SMTP_USER; +const CONTACT_TO = process.env.CONTACT_TO || 'noire.michelle@gmail.com'; + +const transporter = nodemailer.createTransport({ + host: SMTP_HOST, + port: SMTP_PORT, + secure: false, + auth: { user: SMTP_USER, pass: SMTP_PASS }, + tls: { rejectUnauthorized: false } +}); + +const MIME = { + '.html': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.webp': 'image/webp', + '.woff2': 'font/woff2', + '.woff': 'font/woff', +}; + +function serveStatic(req, res) { + let filePath = path.join(STATIC_DIR, req.url === '/' ? 'index.html' : req.url); + filePath = path.normalize(filePath); + if (!filePath.startsWith(STATIC_DIR)) { + res.writeHead(403); + res.end(); + return; + } + + const ext = path.extname(filePath).toLowerCase(); + const contentType = MIME[ext] || 'application/octet-stream'; + + fs.readFile(filePath, (err, data) => { + if (err) { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not found'); + return; + } + res.writeHead(200, { + 'Content-Type': contentType, + 'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=86400' + }); + res.end(data); + }); +} + +function handleContact(req, res) { + let body = ''; + req.on('data', chunk => { body += chunk; }); + req.on('end', async () => { + try { + const { name, email, subject, message } = JSON.parse(body); + + if (!name || !email || !message) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Name, email, and message are required' })); + return; + } + + await transporter.sendMail({ + from: `"Cineasthesia Contact" <${SMTP_FROM}>`, + replyTo: `"${name}" <${email}>`, + to: CONTACT_TO, + subject: `[Cineasthesia] ${subject || 'Contact Form Message'}`, + text: `Name: ${name}\nEmail: ${email}\nSubject: ${subject || '(none)'}\n\nMessage:\n${message}`, + html: ` +
+

New Contact Form Message

+

Name: ${name}

+

Email: ${email}

+

Subject: ${subject || '(none)'}

+
+

${message}

+
+

Sent from cineasthesia.com contact form

+
+ ` + }); + + console.log(`Contact form sent: ${name} <${email}> — ${subject || '(no subject)'}`); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true })); + } catch (err) { + console.error('Contact form error:', err.message); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Failed to send message' })); + } + }); +} + +const server = http.createServer((req, res) => { + if (req.method === 'POST' && req.url === '/api/contact') { + handleContact(req, res); + } else { + serveStatic(req, res); + } +}); + +server.listen(PORT, () => { + console.log(`Cineasthesia server running on port ${PORT}`); + console.log(`Contact emails → ${CONTACT_TO}`); +});