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