Skip to content

Feat/send qr codes#23

Open
kee5625 wants to merge 5 commits into
mainfrom
feat/send-qr-codes
Open

Feat/send qr codes#23
kee5625 wants to merge 5 commits into
mainfrom
feat/send-qr-codes

Conversation

@kee5625

@kee5625 kee5625 commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Description

  • adding 2 new email templates with QR integration for confirmed & waitlisted people
  • updating email send route to include QR rendering logic
  • removing logo and updating faq link in email layout

Checklist when merging to main

  • No compiler warnings (if applicable)
  • Code is formatted with oxfmt or prettier
  • No useless or dead code (if applicable)
  • Code is easy to understand
  • Doc comments are used for all functions, interfaces etc. (where appropriate)
  • Performance has not regressed (assuming change was not to fix a bug)

@vercel

vercel Bot commented Mar 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dashboard-2026 Ready Ready Preview, Comment Mar 28, 2026 8:04am

@kee5625 kee5625 self-assigned this Mar 28, 2026
@greptile-apps

greptile-apps Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds two new QR-code check-in email templates (check-in-confirmed and check-in-waitlisted) and updates the email send route to handle inline QR image attachments. Because Mailgun's batch/recipient-variable mechanism can't share a unique inline attachment per recipient, QR emails are correctly sent one-at-a-time with the QR image attached via CID, while non-QR emails continue to use the existing efficient batch path.

Key changes:

  • QRConfirmed.tsx / QRWaitlisted.tsx — new templates that render cid: inline images; graceful fallback text when qrImageSrc is missing.
  • route.tsbuildQrInlineImage helper normalises stored base64 (handles data: URIs), enforces a 1.5 MB size cap, and adds a pre-flight check that blocks sends when any recipient lacks a QR image.
  • lib/templates/index.ts — registers the new templates and improves type safety by replacing any with Record<string, unknown>.
  • EmailLayout.tsx — removes the logo <Img> from the shared header and redirects the FAQ link to /; both changes affect all existing templates.

Issues found:

  • The sequential per-recipient send loop for QR emails (no concurrency) could be slow or time out for large recipient lists.
  • The logo image was removed from the shared EmailLayout, affecting every email template in the project — worth confirming this is intentional.
  • The logoImage style constant in EmailLayout.tsx is now dead code.
  • isParticipantStatus is validated twice for the status recipient type — the inner check inside the DB block is unreachable.

Confidence Score: 5/5

Safe to merge; all remaining findings are style and performance suggestions with no correctness or data-integrity impact.

The core QR logic — base64 normalisation, size guard, CID attachment, pre-flight missing-QR check — is correct. The non-QR batch path is unchanged. All open findings are P2: sequential sending (performance, not correctness), dead code, a duplicate guard, and a clarification question about the logo removal. None block functionality.

lib/templates/components/EmailLayout.tsx — confirm the logo removal from the shared layout is intentional before merging, as it silently changes every existing email template.

Important Files Changed

Filename Overview
app/api/emails/send/route.ts Adds QR inline-image email path with per-recipient sequential sends and a pre-flight QR validation guard; contains a duplicate isParticipantStatus check and a populated recipientVariables.qrImageSrc field that goes unused during rendering.
lib/templates/QRConfirmed.tsx New QR confirmed email template; well-structured with graceful fallback text when qrImageSrc is absent.
lib/templates/QRWaitlisted.tsx New QR waitlisted email template; mirrors QRConfirmed structure and includes a clear waitlist callout section.
lib/templates/components/EmailLayout.tsx Logo image removed from shared header (affects all templates) and logoImage style constant is now dead code; FAQ link also changed from /faq to /.
lib/templates/index.ts Registers the two new QR templates and tightens type safety by replacing any with Record<string, unknown> and extracting a proper type guard.

Comments Outside Diff (2)

  1. lib/templates/components/EmailLayout.tsx, line 157-159 (link)

    P2 Dead logoImage style constant

    The <Img> that referenced logoImage was removed in this PR, so this style object is now unused. It can be deleted to keep the file clean.

  2. lib/templates/components/EmailLayout.tsx, line 47-53 (link)

    P2 Logo removal affects all existing email templates

    EmailLayout is the shared wrapper used by every template in the project. Removing the logo <Img> here strips it from all outgoing emails — not just the two new QR templates. Was this intentional, or should the logo removal be scoped differently (e.g., an opt-in hideLogo prop)?

Reviews (1): Last reviewed commit: "updating emailLayout and removing logo" | Re-trigger Greptile

Comment on lines 194 to +200
} else if (recipientType === "status") {
if (!isParticipantStatus(status)) {
return NextResponse.json(
{ error: "Invalid participant status" },
{ status: 400 },
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate isParticipantStatus validation

The isParticipantStatus(status) guard at lines 143–148 already returns a 400 before any database work begins, so the identical check inside the DB try block (lines 195–200) can never be reached. The inner check is dead code and can be removed.

Suggested change
} else if (recipientType === "status") {
if (!isParticipantStatus(status)) {
return NextResponse.json(
{ error: "Invalid participant status" },
{ status: 400 },
);
}
} else if (recipientType === "status") {
const statusParticipants = await db

Comment on lines +404 to +471
if (isQrTemplate) {
for (const email of recipients) {
const participant = participantMap.get(email);
const qrInline = participant?.qrInline;
const vars = recipientVariables[email] ?? { firstName: "Hacker" };

if (!qrInline) {
results.push({
recipient: email,
success: false,
error: "Missing QR image data",
});
continue;
}

const [html, text] = await Promise.all([
renderTemplateToHtml(templateId, {
firstName: vars.firstName,
qrImageSrc: qrInline.src,
}),
renderTemplateToText(templateId, {
firstName: vars.firstName,
qrImageSrc: qrInline.src,
}),
]);

if (!html || !text) {
results.push({
recipient: email,
success: false,
error: "Failed to render email template",
});
continue;
}

try {
const response = await mg.messages.create(mailgunDomain, {
from: fromEmail,
to: email,
subject: emailSubject,
html,
text,
inline: [
{
filename: qrInline.filename,
data: qrInline.data,
},
],
});

try {
const response = await mg.messages.create(mailgunDomain, {
from: fromEmail,
to: batch,
subject: emailSubject,
html: html,
text: text,
"recipient-variables": JSON.stringify(batchVars),
});

console.log(
`Batch ${Math.floor(i / BATCH_SIZE) + 1} sent (${batch.length} recipients):`,
response.id,
console.log(`QR email sent to ${email}:`, response.id);
results.push({
recipient: email,
success: true,
messageId: response.id,
});
} catch (error) {
console.error(`QR email send failed for ${email}:`, error);
results.push({
recipient: email,
success: false,
error:
error instanceof Error
? error.message
: "Unknown error",
});
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Sequential QR sends may be slow for large recipient lists

Each iteration awaits both a template render and a Mailgun API call before moving to the next recipient. For a hackathon with hundreds of confirmed attendees, this loop runs entirely in series and could exceed serverless function timeouts (e.g., 60 s on Vercel Pro) or take several minutes even if it completes.

Consider sending a bounded number of emails concurrently — for example, processing recipients in small parallel batches using Promise.allSettled:

const CONCURRENCY = 10;
for (let i = 0; i < recipients.length; i += CONCURRENCY) {
    const slice = recipients.slice(i, i + CONCURRENCY);
    await Promise.allSettled(slice.map(async (email) => { /* send logic */ }));
}

This keeps throughput up while avoiding hammering Mailgun's rate limits.

@kee5625 kee5625 requested a review from AmaanBilwar March 28, 2026 08:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant