馃 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 domain = process.env.DOMAIN || "https://thistle.app"; 100 const verifyLink = `${domain}/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 ${options.className ? ` 214 <p class="info-box-label">Class</p> 215 <p class="info-box-value">${options.className}</p> 216 <hr class="info-box-divider"> 217 ` : ''} 218 <p class="info-box-label">File</p> 219 <p class="info-box-value">${options.originalFilename}</p> 220 </div> 221 222 <p style="text-align: center; margin-top: 1.5rem; margin-bottom: 0;"> 223 <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> 224 </p> 225 </div> 226 <div class="footer"> 227 <p>Thanks for using Thistle!</p> 228 </div> 229 </div> 230</body> 231</html> 232 `.trim(); 233}