馃 distributed transcription service thistle.dunkirk.sh
1/** 2 * Email templates for transactional emails 3 * Uses inline CSS for maximum email client compatibility 4 */ 5 6const baseStyles = ` 7 body { 8 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 9 line-height: 1.6; 10 color: #2d3142; 11 background-color: #ffffff; 12 margin: 0; 13 padding: 0; 14 } 15 .container { 16 max-width: 600px; 17 margin: 0 auto; 18 padding: 2rem 1rem; 19 } 20 .header { 21 text-align: center; 22 margin-bottom: 2rem; 23 } 24 .header h1 { 25 color: #2d3142; 26 font-size: 1.5rem; 27 margin: 0; 28 } 29 .content { 30 background: #ffffff; 31 padding: 2rem; 32 border-radius: 0.5rem; 33 border: 1px solid #bfc0c0; 34 } 35 .button { 36 display: inline-block; 37 background-color: #ef8354; 38 color: #ffffff; 39 text-decoration: none; 40 padding: 0.75rem 1.5rem; 41 border-radius: 6px; 42 font-weight: 500; 43 font-size: 1rem; 44 margin: 1rem 0; 45 border: 2px solid #ef8354; 46 } 47 .footer { 48 text-align: center; 49 margin-top: 2rem; 50 color: #4f5d75; 51 font-size: 0.875rem; 52 } 53 .code { 54 background: #f5f5f5; 55 border: 1px solid #bfc0c0; 56 border-radius: 0.25rem; 57 padding: 0.5rem 1rem; 58 font-family: 'Courier New', monospace; 59 font-size: 1.5rem; 60 letter-spacing: 0.25rem; 61 text-align: center; 62 margin: 1rem 0; 63 } 64 .info-box { 65 background: #f5f5f5; 66 border: 1px solid #bfc0c0; 67 border-radius: 6px; 68 padding: 1.25rem; 69 margin: 1rem 0; 70 } 71 .info-box-label { 72 color: #4f5d75; 73 font-size: 0.75rem; 74 text-transform: uppercase; 75 letter-spacing: 0.05rem; 76 margin: 0 0 0.25rem 0; 77 font-weight: 600; 78 } 79 .info-box-value { 80 color: #2d3142; 81 font-size: 1rem; 82 margin: 0; 83 } 84 .info-box-divider { 85 border: 0; 86 border-top: 1px solid #bfc0c0; 87 margin: 1rem 0; 88 } 89`; 90 91interface VerifyEmailOptions { 92 name: string | null; 93 code: string; 94 token: string; 95} 96 97export function verifyEmailTemplate(options: VerifyEmailOptions): string { 98 const greeting = options.name ? `Hi ${options.name}` : "Hi there"; 99 const origin = process.env.ORIGIN || "http://localhost:3000"; 100 const verifyLink = `${origin}/api/auth/verify-email?token=${options.token}`; 101 102 return ` 103<!DOCTYPE html> 104<html lang="en"> 105<head> 106 <meta charset="UTF-8"> 107 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 108 <title>Verify Your Email - Thistle</title> 109 <style>${baseStyles}</style> 110</head> 111<body> 112 <div class="container"> 113 <div class="header"> 114 <h1>馃 Thistle</h1> 115 </div> 116 <div class="content"> 117 <h2>${greeting}!</h2> 118 <p>Thanks for signing up for Thistle. Please verify your email address to get started.</p> 119 <p><strong>Your verification code is:</strong></p> 120 <div class="code">${options.code}</div> 121 <p style="color: #4f5d75; font-size: 0.875rem; margin-top: 1rem; margin-bottom: 0.75rem;"> 122 This code will expire in 24 hours. Enter it in the verification dialog after you login, or click the button below: 123 </p> 124 <p style="text-align: center; margin-top: 0; margin-bottom: 0.75rem;"> 125 <a href="${verifyLink}" class="button" style="display: inline-block; background-color: #ef8354; color: #ffffff; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 6px; font-weight: 500; font-size: 1rem; margin: 0; border: 2px solid #ef8354;">Verify Email</a> 126 </p> 127 </div> 128 <div class="footer"> 129 <p>If you didn't create an account, you can safely ignore this email.</p> 130 </div> 131 </div> 132</body> 133</html> 134 `.trim(); 135} 136 137interface PasswordResetOptions { 138 name: string | null; 139 resetLink: string; 140} 141 142export function passwordResetTemplate(options: PasswordResetOptions): string { 143 const greeting = options.name ? `Hi ${options.name}` : "Hi there"; 144 145 return ` 146<!DOCTYPE html> 147<html lang="en"> 148<head> 149 <meta charset="UTF-8"> 150 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 151 <title>Reset Your Password - Thistle</title> 152 <style>${baseStyles}</style> 153</head> 154<body> 155 <div class="container"> 156 <div class="header"> 157 <h1>馃 Thistle</h1> 158 </div> 159 <div class="content"> 160 <h2>${greeting}!</h2> 161 <p>We received a request to reset your password. Click the button below to create a new password.</p> 162 <p style="text-align: center;"> 163 <a href="${options.resetLink}" class="button" style="display: inline-block; background-color: #ef8354; color: #ffffff; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 6px; font-weight: 500; font-size: 1rem; margin: 1rem 0; border: 2px solid #ef8354;">Reset Password</a> 164 </p> 165 <p style="color: #4f5d75; font-size: 0.875rem;"> 166 If the button doesn't work, copy and paste this link into your browser:<br> 167 <a href="${options.resetLink}" style="color: #4f5d75; word-break: break-all;">${options.resetLink}</a> 168 </p> 169 <p style="color: #4f5d75; font-size: 0.875rem;"> 170 This link will expire in 1 hour. 171 </p> 172 </div> 173 <div class="footer"> 174 <p>If you didn't request a password reset, you can safely ignore this email.</p> 175 </div> 176 </div> 177</body> 178</html> 179 `.trim(); 180} 181 182interface TranscriptionCompleteOptions { 183 name: string | null; 184 originalFilename: string; 185 transcriptLink: string; 186 className?: string; 187} 188 189export function transcriptionCompleteTemplate( 190 options: TranscriptionCompleteOptions, 191): string { 192 const greeting = options.name ? `Hi ${options.name}` : "Hi there"; 193 194 return ` 195<!DOCTYPE html> 196<html lang="en"> 197<head> 198 <meta charset="UTF-8"> 199 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 200 <title>Transcription Complete - Thistle</title> 201 <style>${baseStyles}</style> 202</head> 203<body> 204 <div class="container"> 205 <div class="header"> 206 <h1>馃 Thistle</h1> 207 </div> 208 <div class="content"> 209 <h2>${greeting}!</h2> 210 <p>Your transcription is ready!</p> 211 212 <div class="info-box"> 213 ${ 214 options.className 215 ? ` 216 <p class="info-box-label">Class</p> 217 <p class="info-box-value">${options.className}</p> 218 <hr class="info-box-divider"> 219 ` 220 : "" 221 } 222 <p class="info-box-label">File</p> 223 <p class="info-box-value">${options.originalFilename}</p> 224 </div> 225 226 <p style="text-align: center; margin-top: 1.5rem; margin-bottom: 0;"> 227 <a href="${options.transcriptLink}" class="button" style="display: inline-block; background-color: #ef8354; color: #ffffff; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 6px; font-weight: 500; font-size: 1rem; margin: 0; border: 2px solid #ef8354;">View Transcript</a> 228 </p> 229 </div> 230 <div class="footer"> 231 <p>Thanks for using Thistle!</p> 232 </div> 233 </div> 234</body> 235</html> 236 `.trim(); 237} 238 239interface EmailChangeOptions { 240 name: string | null; 241 currentEmail: string; 242 newEmail: string; 243 verifyLink: string; 244} 245 246export function emailChangeTemplate(options: EmailChangeOptions): string { 247 const greeting = options.name ? `Hi ${options.name}` : "Hi there"; 248 249 return ` 250<!DOCTYPE html> 251<html lang="en"> 252<head> 253 <meta charset="UTF-8"> 254 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 255 <title>Verify Email Change - Thistle</title> 256 <style>${baseStyles}</style> 257</head> 258<body> 259 <div class="container"> 260 <div class="header"> 261 <h1>馃 Thistle</h1> 262 </div> 263 <div class="content"> 264 <h2>${greeting}!</h2> 265 <p>You requested to change your email address.</p> 266 267 <div class="info-box"> 268 <p class="info-box-label">Current Email</p> 269 <p class="info-box-value">${options.currentEmail}</p> 270 <hr class="info-box-divider"> 271 <p class="info-box-label">New Email</p> 272 <p class="info-box-value">${options.newEmail}</p> 273 </div> 274 275 <p>Click the button below to confirm this change:</p> 276 277 <p style="text-align: center; margin-top: 1.5rem; margin-bottom: 0;"> 278 <a href="${options.verifyLink}" class="button" style="display: inline-block; background-color: #ef8354; color: #ffffff; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 6px; font-weight: 500; font-size: 1rem; margin: 0; border: 2px solid #ef8354;">Verify Email Change</a> 279 </p> 280 281 <p style="color: #4f5d75; font-size: 0.875rem; margin-top: 1.5rem;"> 282 If the button doesn't work, copy and paste this link into your browser:<br> 283 <a href="${options.verifyLink}" style="color: #4f5d75; word-break: break-all;">${options.verifyLink}</a> 284 </p> 285 286 <p style="color: #4f5d75; font-size: 0.875rem;"> 287 This link will expire in 24 hours. 288 </p> 289 </div> 290 <div class="footer"> 291 <p>If you didn't request this change, please ignore this email and your email address will remain unchanged.</p> 292 </div> 293 </div> 294</body> 295</html> 296 `.trim(); 297}