chore: add server/email.ts

This commit is contained in:
notshop 2026-04-26 16:36:17 +00:00
parent 3812bbe6ae
commit 2fb134f42b

122
server/email.ts Normal file
View file

@ -0,0 +1,122 @@
/**
* Email service powered by Resend.
*
* Required env vars:
* RESEND_API_KEY
* STORE_EMAIL_FROM (e.g. "orders@yourstore.com")
* STORE_NAME (e.g. "My Store")
*/
import { Resend } from "resend";
let _resend: Resend | null = null;
function getResend(): Resend {
if (!_resend) {
if (!process.env.RESEND_API_KEY) {
throw new Error("RESEND_API_KEY is not configured");
}
_resend = new Resend(process.env.RESEND_API_KEY);
}
return _resend;
}
const FROM = process.env.STORE_EMAIL_FROM || "noreply@yourstore.com";
const STORE_NAME = process.env.STORE_NAME || "My Store";
export async function sendMagicLink(email: string, token: string, baseUrl: string) {
const link = `${baseUrl}/account/verify?token=${token}`;
try {
await getResend().emails.send({
from: FROM,
to: email,
subject: `Sign in to ${STORE_NAME}`,
html: `
<p>Click the link below to sign in to ${STORE_NAME}. This link expires in 15 minutes.</p>
<p><a href="${link}" style="font-weight:bold">Sign In </a></p>
<p style="color:#888;font-size:12px">If you didn't request this, you can safely ignore this email.</p>
`,
});
} catch (err) {
console.error("[email] sendMagicLink failed:", err);
}
}
export async function sendOrderConfirmation(params: {
email: string;
orderNumber: string;
total: string;
items: Array<{ title: string; variantTitle?: string; quantity: number; price: string }>;
shippingAddress?: any;
}) {
const itemsHtml = params.items
.map(
(item) => `<tr>
<td style="padding:8px 0;border-bottom:1px solid #f0f0f0">${item.title}${item.variantTitle ? `${item.variantTitle}` : ""}</td>
<td style="padding:8px 0;border-bottom:1px solid #f0f0f0;text-align:center">${item.quantity}</td>
<td style="padding:8px 0;border-bottom:1px solid #f0f0f0;text-align:right">$${item.price}</td>
</tr>`
)
.join("");
const addr = params.shippingAddress;
const addrHtml = addr
? `<p><strong>Ship to:</strong><br>${addr.firstName} ${addr.lastName}<br>${addr.address1}${addr.address2 ? `, ${addr.address2}` : ""}<br>${addr.city}, ${addr.state} ${addr.zip}</p>`
: "";
try {
await getResend().emails.send({
from: FROM,
to: params.email,
subject: `Order confirmed — #${params.orderNumber}`,
html: `
<div style="font-family:sans-serif;max-width:560px;margin:auto">
<h2 style="color:#333">Thanks for your order!</h2>
<p>Order <strong>#${params.orderNumber}</strong> has been received and is being processed.</p>
<table width="100%" cellpadding="0" cellspacing="0">
<thead>
<tr>
<th style="text-align:left;padding-bottom:8px;border-bottom:2px solid #eee">Item</th>
<th style="text-align:center;padding-bottom:8px;border-bottom:2px solid #eee">Qty</th>
<th style="text-align:right;padding-bottom:8px;border-bottom:2px solid #eee">Price</th>
</tr>
</thead>
<tbody>${itemsHtml}</tbody>
<tfoot>
<tr>
<td colspan="2" style="padding-top:12px;text-align:right;font-weight:bold">Total</td>
<td style="padding-top:12px;text-align:right;font-weight:bold">$${params.total}</td>
</tr>
</tfoot>
</table>
${addrHtml}
<p style="color:#888;font-size:12px;margin-top:32px">Thank you for shopping with ${STORE_NAME}.</p>
</div>
`,
});
} catch (err) {
console.error("[email] sendOrderConfirmation failed:", err);
}
}
export async function sendShippingNotification(params: {
email: string;
orderNumber: string;
trackingNumber: string;
trackingCarrier?: string;
}) {
try {
await getResend().emails.send({
from: FROM,
to: params.email,
subject: `Your order #${params.orderNumber} has shipped`,
html: `
<p>Great news! Your ${STORE_NAME} order <strong>#${params.orderNumber}</strong> is on its way.</p>
<p><strong>Tracking number:</strong> ${params.trackingNumber}${params.trackingCarrier ? ` (${params.trackingCarrier})` : ""}</p>
<p style="color:#888;font-size:12px">Thank you for your order!</p>
`,
});
} catch (err) {
console.error("[email] sendShippingNotification failed:", err);
}
}