brackt/app/services/draft-email.server.ts
Claude 2ef75a1d44
Add on-the-clock email notifications and fix push notification modal
- Add `draft_email_notifications_enabled` column to users table
- New `sendOnTheClockEmail` service sends an email when a user's turn arrives
  in a draft; detects back-to-back picks and sends one combined email instead
  of two; skips notification if the team has autodraft enabled
- Email is triggered after every manual pick (make-pick route) and every
  timer-expiry pick (executeAutoPick) once the full autodraft chain settles,
  so the notified user is always the first human on the clock
- Add email notification toggle to the user settings Notifications section
- Fix push notification dropdown in the draft room: the bare absolute div is
  replaced with a Radix Popover so clicking outside dismisses it without
  toggling notifications off
- Rename draft room label from "Pick Notifications" to "Push Notifications"
  to distinguish from the new email notifications

https://claude.ai/code/session_01LMGxgYvtE3CF8Jf3u2pXgA
2026-05-20 21:33:20 +00:00

107 lines
4.3 KiB
TypeScript

import { eq, and } from "drizzle-orm";
import * as schema from "~/database/schema";
import { sendEmail, wrapInEmailTemplate, emailParagraph, emailButton, escapeHtml } from "~/lib/email.server";
import { calculatePickInfo } from "~/models/draft-utils";
import { logger } from "~/lib/logger";
import type { database } from "~/database/context";
type DraftSlot = { teamId: string; draftOrder: number };
export async function sendOnTheClockEmail(params: {
seasonId: string;
currentPickNumber: number;
totalTeams: number;
totalPicks: number;
draftSlots: DraftSlot[];
db: ReturnType<typeof database>;
}): Promise<void> {
const { seasonId, currentPickNumber, totalTeams, totalPicks, draftSlots, db } = params;
if (currentPickNumber > totalPicks) return;
const { pickInRound } = calculatePickInfo(currentPickNumber, totalTeams);
const currentSlot = draftSlots.find((s) => s.draftOrder === pickInRound);
if (!currentSlot) return;
// Fetch team + owner user
const team = await db.query.teams.findFirst({
where: eq(schema.teams.id, currentSlot.teamId),
});
if (!team?.ownerId) return;
const owner = await db.query.users.findFirst({
where: eq(schema.users.id, team.ownerId as string),
});
if (!owner?.email || !owner.draftEmailNotificationsEnabled) return;
// Safety net: skip if this team has autodraft enabled (their pick will be made automatically)
const autodraft = await db.query.autodraftSettings.findFirst({
where: and(
eq(schema.autodraftSettings.seasonId, seasonId),
eq(schema.autodraftSettings.teamId, currentSlot.teamId)
),
});
if (autodraft?.isEnabled) return;
// Detect consecutive picks (same team picks the very next slot too)
let isConsecutive = false;
if (currentPickNumber + 1 <= totalPicks) {
const { pickInRound: nextPickInRound } = calculatePickInfo(currentPickNumber + 1, totalTeams);
const nextSlot = draftSlots.find((s) => s.draftOrder === nextPickInRound);
if (nextSlot?.teamId === currentSlot.teamId) {
isConsecutive = true;
}
}
// Fetch season + league for context
const season = await db.query.seasons.findFirst({
where: eq(schema.seasons.id, seasonId),
});
if (!season) return;
const league = await db.query.leagues.findFirst({
where: eq(schema.leagues.id, season.leagueId),
});
if (!league) return;
const appUrl = process.env.APP_URL ?? "https://brackt.com";
const draftUrl = `${appUrl}/leagues/${season.leagueId}/draft/${seasonId}`;
const leagueName = escapeHtml(league.name);
const teamName = escapeHtml(team.name);
const { round } = calculatePickInfo(currentPickNumber, totalTeams);
let subject: string;
let preheader: string;
let bodyHtml: string;
if (isConsecutive) {
const { round: round2 } = calculatePickInfo(currentPickNumber + 1, totalTeams);
subject = `You have back-to-back picks in ${league.name}`;
preheader = `${team.name} picks twice in a row — head to the draft room!`;
bodyHtml = `
${emailParagraph(`<strong>You're up twice in a row!</strong>`)}
${emailParagraph(`<strong>${teamName}</strong> has back-to-back picks (Round ${round} and Round ${round2}) in <strong>${leagueName}</strong>. Head to the draft room to make both picks before time runs out.`)}
${emailButton(draftUrl, "Go to Draft Room")}
${emailParagraph(`<span style="font-size:13px;color:rgba(255,255,255,0.6);">You're receiving this because you enabled draft email notifications in your account settings.</span>`)}
`;
} else {
subject = `You're on the clock in ${league.name}`;
preheader = `${team.name} is on the clock — make your pick!`;
bodyHtml = `
${emailParagraph(`<strong>You're on the clock!</strong>`)}
${emailParagraph(`<strong>${teamName}</strong> is now on the clock in Round ${round} of <strong>${leagueName}</strong>. Head to the draft room to make your pick before time runs out.`)}
${emailButton(draftUrl, "Go to Draft Room")}
${emailParagraph(`<span style="font-size:13px;color:rgba(255,255,255,0.6);">You're receiving this because you enabled draft email notifications in your account settings.</span>`)}
`;
}
const { error } = await sendEmail({
to: owner.email,
subject,
html: wrapInEmailTemplate(bodyHtml, preheader),
});
if (error) {
logger.error(`[DraftEmail] Failed to send on-the-clock email to ${owner.email}:`, error);
}
}