feat(storage): MinIO object storage, Neon Auth, Supabase removal
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject, deleteObject, presigned URL helpers, and BUCKETS constant - Wire product images, brand logos, and water log photos to MinIO via the new storage client - Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call) - Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge Function proxy) - Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass brand_settings.logo_url from all call sites - Update email templates to use dynamic logoUrl instead of hardcoded Supabase bucket URLs - Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage, TimeTrackingFieldClient; use brand_settings props + local public/ fallback - Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to public/ for local development - Add MinIO env vars to .env.example (endpoint, access key, secret, buckets) - Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props - Fix admin/users.ts logoUrl type (null → undefined for optional string) - Remove stale sb- cookie from wholesale-auth - Migrate tuxedo/about page to remove supabase import and use pool query for wholesale_settings lookup
This commit is contained in:
+55
-15
@@ -53,6 +53,27 @@ async function sendEmail(opts: {
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Generic campaign email — used by the scheduled-campaigns cron
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type CampaignEmailData = {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
from?: string;
|
||||
};
|
||||
|
||||
export async function sendCampaignEmail(data: CampaignEmailData): Promise<boolean> {
|
||||
return sendEmail({
|
||||
to: data.to,
|
||||
subject: data.subject,
|
||||
html: data.html,
|
||||
text: data.subject,
|
||||
from: data.from,
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Order Receipt — sent to customer after successful checkout
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -76,6 +97,8 @@ export type OrderReceiptData = {
|
||||
stopTime?: string;
|
||||
stopLocation?: string;
|
||||
brandName?: string;
|
||||
/** Public URL for the brand logo (e.g. from brand_settings.logo_url). */
|
||||
logoUrl?: string | null;
|
||||
};
|
||||
|
||||
export async function sendOrderReceiptEmail(data: OrderReceiptData): Promise<boolean> {
|
||||
@@ -106,20 +129,29 @@ export async function sendOrderReceiptEmail(data: OrderReceiptData): Promise<boo
|
||||
<p style="margin: 0; font-size: 14px; font-weight: 600; color: #166534;">Shipping to your door — cooler boxes ship after the season ends.</p>
|
||||
</div>`;
|
||||
|
||||
const logoHeader = data.logoUrl
|
||||
? `<img src="${data.logoUrl}" alt="${data.brandName ?? ""}" style="height: 40px; width: auto; object-fit: contain; margin-bottom: 16px; filter: brightness(0) invert(1); opacity: 0.9;" />`
|
||||
: data.brandName
|
||||
? `<p style="margin: 0 0 16px; font-size: 18px; font-weight: 800; color: #ffffff; letter-spacing: -0.02em;">${data.brandName}</p>`
|
||||
: "";
|
||||
|
||||
const logoCallout = data.logoUrl
|
||||
? `<img src="${data.logoUrl}" alt="${data.brandName ?? ""}" style="height: 28px; width: auto; object-fit: contain; opacity: 0.7; flex-shrink: 0;" />`
|
||||
: "";
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Order Confirmed — Tuxedo Corn</title>
|
||||
<title>Order Confirmed — ${data.brandName ?? "Tuxedo Corn"}</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background: #fafaf9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1c1917;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 32px 16px;">
|
||||
|
||||
<!-- Header -->
|
||||
<div style="background: #1c1917; border-radius: 16px; padding: 32px; text-align: center; margin-bottom: 24px;">
|
||||
<img src="https://wnzkhezyhnfzhkhiflrp.supabase.co/storage/v1/object/public/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo.png"
|
||||
alt="Olathe Sweet" style="height: 40px; width: auto; object-fit: contain; margin-bottom: 16px; filter: brightness(0) invert(1); opacity: 0.9;" />
|
||||
${logoHeader}
|
||||
<h1 style="margin: 0; font-size: 28px; font-weight: 800; color: #ffffff; letter-spacing: -0.02em;">Order Confirmed</h1>
|
||||
<p style="margin: 8px 0 0; font-size: 14px; color: rgba(255,255,255,0.6);">Order #${data.orderId.slice(0, 8).toUpperCase()}</p>
|
||||
</div>
|
||||
@@ -165,14 +197,14 @@ export async function sendOrderReceiptEmail(data: OrderReceiptData): Promise<boo
|
||||
<!-- Pickup / Shipping details -->
|
||||
${pickupBlock}
|
||||
|
||||
<!-- Olathe Sweet callout -->
|
||||
<!-- Brand callout -->
|
||||
${logoCallout || data.brandName ? `
|
||||
<div style="background: #fafaf9; border-radius: 10px; padding: 16px 20px; margin-top: 24px; display: flex; align-items: center; gap: 12px;">
|
||||
<img src="https://wnzkhezyhnfzhkhiflrp.supabase.co/storage/v1/object/public/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo.png"
|
||||
alt="Olathe Sweet" style="height: 28px; width: auto; object-fit: contain; opacity: 0.7; flex-shrink: 0;" />
|
||||
${logoCallout}
|
||||
<p style="margin: 0; font-size: 12px; color: #78716c; line-height: 1.5;">
|
||||
Grown in Olathe, Colorado — Non-GMO · Hand-Picked · Farm-Direct
|
||||
${data.brandName ? `Grown in Olathe, Colorado — Non-GMO · Hand-Picked · Farm-Direct` : ""}
|
||||
</p>
|
||||
</div>
|
||||
</div>` : ""}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
@@ -181,7 +213,7 @@ export async function sendOrderReceiptEmail(data: OrderReceiptData): Promise<boo
|
||||
Questions? Reply to this email or contact us at <a href="mailto:hello@tuxedocorn.com" style="color: #78716c;">hello@tuxedocorn.com</a>
|
||||
</p>
|
||||
<p style="margin: 8px 0 0; font-size: 11px; color: #d6d3d1;">
|
||||
© ${new Date().getFullYear()} Tuxedo Corn · 59751 David Road, Olathe, CO 81425
|
||||
© ${new Date().getFullYear()} ${data.brandName ?? "Tuxedo Corn"} · 59751 David Road, Olathe, CO 81425
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -224,6 +256,8 @@ export type WelcomeEmailData = {
|
||||
brandName?: string;
|
||||
tempPassword?: string;
|
||||
setupUrl?: string;
|
||||
/** Public URL for the brand logo (e.g. from brand_settings.logo_url). */
|
||||
logoUrl?: string | null;
|
||||
};
|
||||
|
||||
export async function sendWelcomeEmail(data: WelcomeEmailData): Promise<boolean> {
|
||||
@@ -257,20 +291,25 @@ export async function sendWelcomeEmail(data: WelcomeEmailData): Promise<boolean>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const logoHeader = data.logoUrl
|
||||
? `<img src="${data.logoUrl}" alt="${data.brandName ?? ""}" style="height: 36px; width: auto; object-fit: contain; margin-bottom: 16px;" />`
|
||||
: data.brandName
|
||||
? `<p style="margin: 0 0 16px; font-size: 18px; font-weight: 800; color: #ffffff; letter-spacing: -0.02em;">${data.brandName}</p>`
|
||||
: "";
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Welcome to Tuxedo Corn</title>
|
||||
<title>Welcome to ${data.brandName ?? "Route Commerce"}</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background: #fafaf9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1c1917;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 32px 16px;">
|
||||
|
||||
<!-- Header -->
|
||||
<div style="background: #1c1917; border-radius: 16px; padding: 32px; text-align: center; margin-bottom: 24px;">
|
||||
<img src="https://wnzkhezyhnfzhkhiflrp.supabase.co/storage/v1/object/public/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/logo.png"
|
||||
alt="Tuxedo Corn" style="height: 36px; width: auto; object-fit: contain; margin-bottom: 16px;" />
|
||||
${logoHeader}
|
||||
<h1 style="margin: 0; font-size: 26px; font-weight: 800; color: #ffffff; letter-spacing: -0.02em;">Welcome, ${data.name.split(" ")[0]}</h1>
|
||||
<p style="margin: 8px 0 0; font-size: 14px; color: rgba(255,255,255,0.6);">Your ${roleLabel} account is ready</p>
|
||||
${brandBlock}
|
||||
@@ -292,7 +331,7 @@ export async function sendWelcomeEmail(data: WelcomeEmailData): Promise<boolean>
|
||||
<!-- Footer -->
|
||||
<div style="text-align: center; margin-top: 24px;">
|
||||
<p style="margin: 0; font-size: 12px; color: #a8a29e;">
|
||||
© ${new Date().getFullYear()} Tuxedo Corn · Route Commerce Platform
|
||||
© ${new Date().getFullYear()} ${data.brandName ?? "Route Commerce"} · Route Commerce Platform
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -337,6 +376,8 @@ export type PasswordResetData = {
|
||||
to: string;
|
||||
name: string;
|
||||
resetUrl: string;
|
||||
/** Public URL for the brand logo (e.g. from brand_settings.logo_url). */
|
||||
logoUrl?: string | null;
|
||||
};
|
||||
|
||||
export async function sendPasswordResetEmail(data: PasswordResetData): Promise<boolean> {
|
||||
@@ -350,8 +391,7 @@ export async function sendPasswordResetEmail(data: PasswordResetData): Promise<b
|
||||
<body style="margin: 0; padding: 0; background: #fafaf9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1c1917;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 32px 16px;">
|
||||
<div style="background: #1c1917; border-radius: 16px; padding: 32px; text-align: center; margin-bottom: 24px;">
|
||||
<img src="https://wnzkhezyhnfzhkhiflrp.supabase.co/storage/v1/object/public/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/logo.png"
|
||||
alt="Tuxedo Corn" style="height: 32px; width: auto; object-fit: contain; margin-bottom: 16px;" />
|
||||
${data.logoUrl ? `<img src="${data.logoUrl}" alt="" style="height: 32px; width: auto; object-fit: contain; margin-bottom: 16px;" />` : ""}
|
||||
<h1 style="margin: 0; font-size: 24px; font-weight: 800; color: #ffffff;">Reset Your Password</h1>
|
||||
</div>
|
||||
<div style="background: #ffffff; border-radius: 16px; padding: 32px; border: 1px solid #e7e5e4;">
|
||||
|
||||
Reference in New Issue
Block a user