馃 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}