export interface SlackMessage { text?: string; blocks?: SlackBlock[]; channel?: string; } interface SlackBlock { type: string; text?: { type: string; text: string }; elements?: unknown[]; } /** * Send a message to a Slack webhook URL. */ export async function sendSlackMessage( webhookUrl: string, message: SlackMessage ): Promise { const res = await fetch(webhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message), }); if (!res.ok) { const body = await res.text(); console.error("[Slack] Webhook failed:", res.status, body); } } /** * Send a plain text Slack notification. */ export async function sendSlackText( webhookUrl: string, text: string ): Promise { return sendSlackMessage(webhookUrl, { text }); } // ── Pre-built notification templates ───────────────────────────────────────── export async function slackNotifyVersionUploaded( webhookUrl: string, params: { shotCode: string; versionLabel: string; artistName: string; projectName: string; reviewUrl: string; } ) { await sendSlackMessage(webhookUrl, { blocks: [ { type: "section", text: { type: "mrkdwn", text: `🎬 *New version uploaded*\n*${params.projectName}* · ${params.shotCode} ${params.versionLabel} by ${params.artistName}`, }, }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "Review Now" }, url: params.reviewUrl, style: "primary", }, ], }, ], }); } export async function slackNotifyApproval( webhookUrl: string, params: { shotCode: string; versionLabel: string; status: "APPROVED" | "REJECTED" | "NEEDS_CHANGES"; reviewerName: string; projectName: string; reviewUrl: string; } ) { const icons = { APPROVED: "✅", REJECTED: "❌", NEEDS_CHANGES: "⚠️", }; const labels = { APPROVED: "approved", REJECTED: "rejected", NEEDS_CHANGES: "needs changes", }; const icon = icons[params.status]; const label = labels[params.status]; await sendSlackMessage(webhookUrl, { blocks: [ { type: "section", text: { type: "mrkdwn", text: `${icon} *${params.shotCode} ${params.versionLabel} ${label}*\nBy ${params.reviewerName} · ${params.projectName}`, }, }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "View Version" }, url: params.reviewUrl, }, ], }, ], }); } export async function slackNotifyTaskReadyForReview( webhookUrl: string, params: { taskTitle: string; contextCode: string | null; artistName: string; projectName: string; taskUrl: string; } ) { const label = params.contextCode ? `${params.contextCode} — ` : ""; await sendSlackMessage(webhookUrl, { blocks: [ { type: "section", text: { type: "mrkdwn", text: `👁 *Task ready for review*\n*${params.projectName}* · ${label}${params.taskTitle}\nSubmitted by ${params.artistName}`, }, }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "Review Task" }, url: params.taskUrl, style: "primary", }, ], }, ], }); } export async function slackNotifyTaskAssigned( webhookUrl: string, params: { taskTitle: string; contextCode: string | null; artistName: string; assignedByName: string; projectName: string; taskUrl: string; } ) { const label = params.contextCode ? `${params.contextCode} — ` : ""; await sendSlackMessage(webhookUrl, { blocks: [ { type: "section", text: { type: "mrkdwn", text: `📋 *Task assigned*\n*${params.projectName}* · ${label}${params.taskTitle}\nAssigned to ${params.artistName} by ${params.assignedByName}`, }, }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "View Task" }, url: params.taskUrl, }, ], }, ], }); } export async function slackNotifyNewFeedback( webhookUrl: string, params: { shotCode: string; frameNumber: number; authorName: string; commentText: string; reviewUrl: string; } ) { const truncated = params.commentText.length > 120 ? params.commentText.slice(0, 117) + "..." : params.commentText; await sendSlackMessage(webhookUrl, { blocks: [ { type: "section", text: { type: "mrkdwn", text: `💬 *New feedback* on ${params.shotCode} frame ${params.frameNumber}\n*${params.authorName}:* ${truncated}`, }, }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", text: "Jump to Frame" }, url: params.reviewUrl, }, ], }, ], }); }