@prmichaelsen/acp-visualizer 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/package.json +8 -10
  2. package/src/components/ExtraFieldsBadge.tsx +1 -1
  3. package/src/components/FilterBar.tsx +1 -1
  4. package/src/components/Header.tsx +1 -1
  5. package/src/components/MilestoneTable.tsx +1 -1
  6. package/src/components/MilestoneTree.tsx +2 -2
  7. package/src/components/StatusBadge.tsx +1 -1
  8. package/src/components/StatusDot.tsx +1 -1
  9. package/src/components/TaskList.tsx +1 -1
  10. package/src/routes/__root.tsx +5 -5
  11. package/src/routes/api/watch.ts +1 -1
  12. package/src/routes/index.tsx +2 -2
  13. package/src/routes/milestones.tsx +7 -7
  14. package/src/routes/search.tsx +4 -4
  15. package/src/routes/tasks.tsx +3 -3
  16. package/src/services/progress-database.service.ts +3 -3
  17. package/agent/commands/acp.clarification-address.md +0 -417
  18. package/agent/commands/acp.clarification-capture.md +0 -386
  19. package/agent/commands/acp.clarification-create.md +0 -437
  20. package/agent/commands/acp.clarifications-research.md +0 -326
  21. package/agent/commands/acp.command-create.md +0 -432
  22. package/agent/commands/acp.design-create.md +0 -286
  23. package/agent/commands/acp.design-reference.md +0 -355
  24. package/agent/commands/acp.handoff.md +0 -270
  25. package/agent/commands/acp.index.md +0 -423
  26. package/agent/commands/acp.init.md +0 -546
  27. package/agent/commands/acp.package-create.md +0 -895
  28. package/agent/commands/acp.package-info.md +0 -212
  29. package/agent/commands/acp.package-install.md +0 -539
  30. package/agent/commands/acp.package-list.md +0 -280
  31. package/agent/commands/acp.package-publish.md +0 -541
  32. package/agent/commands/acp.package-remove.md +0 -293
  33. package/agent/commands/acp.package-search.md +0 -307
  34. package/agent/commands/acp.package-update.md +0 -361
  35. package/agent/commands/acp.package-validate.md +0 -540
  36. package/agent/commands/acp.pattern-create.md +0 -386
  37. package/agent/commands/acp.plan.md +0 -587
  38. package/agent/commands/acp.proceed.md +0 -882
  39. package/agent/commands/acp.project-create.md +0 -675
  40. package/agent/commands/acp.project-info.md +0 -312
  41. package/agent/commands/acp.project-list.md +0 -226
  42. package/agent/commands/acp.project-remove.md +0 -379
  43. package/agent/commands/acp.project-set.md +0 -227
  44. package/agent/commands/acp.project-update.md +0 -307
  45. package/agent/commands/acp.projects-restore.md +0 -228
  46. package/agent/commands/acp.projects-sync.md +0 -347
  47. package/agent/commands/acp.report.md +0 -407
  48. package/agent/commands/acp.resume.md +0 -239
  49. package/agent/commands/acp.sessions.md +0 -301
  50. package/agent/commands/acp.status.md +0 -293
  51. package/agent/commands/acp.sync.md +0 -364
  52. package/agent/commands/acp.task-create.md +0 -500
  53. package/agent/commands/acp.update.md +0 -302
  54. package/agent/commands/acp.validate.md +0 -466
  55. package/agent/commands/acp.version-check-for-updates.md +0 -276
  56. package/agent/commands/acp.version-check.md +0 -191
  57. package/agent/commands/acp.version-update.md +0 -289
  58. package/agent/commands/command.template.md +0 -339
  59. package/agent/commands/git.commit.md +0 -526
  60. package/agent/commands/git.init.md +0 -514
  61. package/agent/commands/tanstack-cloudflare.deploy.md +0 -272
  62. package/agent/commands/tanstack-cloudflare.tail.md +0 -275
  63. package/agent/design/.gitkeep +0 -0
  64. package/agent/design/design.template.md +0 -154
  65. package/agent/design/local.dashboard-layout-routing.md +0 -288
  66. package/agent/design/local.data-model-yaml-parsing.md +0 -310
  67. package/agent/design/local.search-filtering.md +0 -331
  68. package/agent/design/local.server-api-auto-refresh.md +0 -235
  69. package/agent/design/local.table-tree-views.md +0 -299
  70. package/agent/design/local.visualizer-requirements.md +0 -349
  71. package/agent/design/requirements.template.md +0 -387
  72. package/agent/index/.gitkeep +0 -0
  73. package/agent/index/acp.core.yaml +0 -137
  74. package/agent/index/local.main.template.yaml +0 -37
  75. package/agent/manifest.template.yaml +0 -13
  76. package/agent/manifest.yaml +0 -302
  77. package/agent/milestones/.gitkeep +0 -0
  78. package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +0 -67
  79. package/agent/milestones/milestone-1-{title}.template.md +0 -206
  80. package/agent/milestones/milestone-2-dashboard-views-interaction.md +0 -79
  81. package/agent/package.template.yaml +0 -86
  82. package/agent/patterns/.gitkeep +0 -0
  83. package/agent/patterns/bootstrap.template.md +0 -1237
  84. package/agent/patterns/pattern.template.md +0 -382
  85. package/agent/patterns/tanstack-cloudflare.acl-permissions.md +0 -332
  86. package/agent/patterns/tanstack-cloudflare.action-bar-item.md +0 -416
  87. package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +0 -401
  88. package/agent/patterns/tanstack-cloudflare.auth-session-management.md +0 -387
  89. package/agent/patterns/tanstack-cloudflare.card-and-list.md +0 -271
  90. package/agent/patterns/tanstack-cloudflare.chat-engine.md +0 -353
  91. package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +0 -346
  92. package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +0 -516
  93. package/agent/patterns/tanstack-cloudflare.email-service.md +0 -431
  94. package/agent/patterns/tanstack-cloudflare.expander.md +0 -98
  95. package/agent/patterns/tanstack-cloudflare.fcm-push.md +0 -115
  96. package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +0 -441
  97. package/agent/patterns/tanstack-cloudflare.firebase-auth.md +0 -348
  98. package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +0 -550
  99. package/agent/patterns/tanstack-cloudflare.firebase-storage.md +0 -369
  100. package/agent/patterns/tanstack-cloudflare.form-controls.md +0 -145
  101. package/agent/patterns/tanstack-cloudflare.global-search-context.md +0 -93
  102. package/agent/patterns/tanstack-cloudflare.image-carousel.md +0 -126
  103. package/agent/patterns/tanstack-cloudflare.library-services.md +0 -553
  104. package/agent/patterns/tanstack-cloudflare.lightbox.md +0 -169
  105. package/agent/patterns/tanstack-cloudflare.markdown-content.md +0 -115
  106. package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +0 -98
  107. package/agent/patterns/tanstack-cloudflare.modal.md +0 -156
  108. package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +0 -461
  109. package/agent/patterns/tanstack-cloudflare.notifications-engine.md +0 -151
  110. package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +0 -90
  111. package/agent/patterns/tanstack-cloudflare.og-metadata.md +0 -296
  112. package/agent/patterns/tanstack-cloudflare.pagination.md +0 -442
  113. package/agent/patterns/tanstack-cloudflare.pill-input.md +0 -220
  114. package/agent/patterns/tanstack-cloudflare.provider-adapter.md +0 -401
  115. package/agent/patterns/tanstack-cloudflare.rate-limiting.md +0 -323
  116. package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +0 -338
  117. package/agent/patterns/tanstack-cloudflare.searchable-settings.md +0 -375
  118. package/agent/patterns/tanstack-cloudflare.slide-over.md +0 -129
  119. package/agent/patterns/tanstack-cloudflare.ssr-preload.md +0 -571
  120. package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +0 -508
  121. package/agent/patterns/tanstack-cloudflare.toast-system.md +0 -142
  122. package/agent/patterns/tanstack-cloudflare.unified-header.md +0 -280
  123. package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +0 -628
  124. package/agent/patterns/tanstack-cloudflare.websocket-manager.md +0 -237
  125. package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +0 -358
  126. package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +0 -336
  127. package/agent/progress.template.yaml +0 -161
  128. package/agent/progress.yaml +0 -145
  129. package/agent/schemas/package.schema.yaml +0 -276
  130. package/agent/scripts/acp.common.sh +0 -1781
  131. package/agent/scripts/acp.install.sh +0 -333
  132. package/agent/scripts/acp.package-create.sh +0 -924
  133. package/agent/scripts/acp.package-info.sh +0 -288
  134. package/agent/scripts/acp.package-install.sh +0 -893
  135. package/agent/scripts/acp.package-list.sh +0 -311
  136. package/agent/scripts/acp.package-publish.sh +0 -420
  137. package/agent/scripts/acp.package-remove.sh +0 -348
  138. package/agent/scripts/acp.package-search.sh +0 -156
  139. package/agent/scripts/acp.package-update.sh +0 -517
  140. package/agent/scripts/acp.package-validate.sh +0 -1018
  141. package/agent/scripts/acp.uninstall.sh +0 -85
  142. package/agent/scripts/acp.version-check-for-updates.sh +0 -98
  143. package/agent/scripts/acp.version-check.sh +0 -47
  144. package/agent/scripts/acp.version-update.sh +0 -176
  145. package/agent/scripts/acp.yaml-parser.sh +0 -985
  146. package/agent/scripts/acp.yaml-validate.sh +0 -205
  147. package/agent/tasks/.gitkeep +0 -0
  148. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +0 -210
  149. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +0 -294
  150. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +0 -193
  151. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +0 -262
  152. package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +0 -156
  153. package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +0 -178
  154. package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +0 -141
  155. package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +0 -153
  156. package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +0 -174
  157. package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +0 -233
  158. package/agent/tasks/task-1-{title}.template.md +0 -244
  159. package/vitest.config.ts +0 -27
@@ -1,431 +0,0 @@
1
- # Email Service Pattern
2
-
3
- **Category**: Architecture
4
- **Applicable To**: TanStack Start + Cloudflare Workers applications that send transactional emails
5
- **Status**: Stable
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- This pattern provides a lightweight, provider-agnostic email service for sending transactional emails (notifications, digests, confirmations, password resets) from Cloudflare Workers. It wraps email API providers (Mandrill, SendGrid, Resend, etc.) behind a simple `sendEmail({ to, subject, html })` interface and includes HTML template builders for common email types.
12
-
13
- The pattern enforces non-blocking email sending — email failures are logged but never crash the calling operation. A user creating a post should never fail because the notification email couldn't be sent.
14
-
15
- ---
16
-
17
- ## When to Use This Pattern
18
-
19
- ✅ **Use this pattern when:**
20
- - Need to send transactional emails (confirmations, notifications, digests)
21
- - Using a third-party email API (Mandrill, SendGrid, Resend, Mailgun)
22
- - Want a simple, mockable email interface
23
- - Need HTML email templates
24
-
25
- ❌ **Don't use this pattern when:**
26
- - Only sending emails via a marketing platform (Mailchimp campaigns)
27
- - Using a full email framework (Nodemailer with SMTP — not available on Workers)
28
- - Email is not a feature of your application
29
-
30
- ---
31
-
32
- ## Core Principles
33
-
34
- 1. **Simple Interface**: `sendEmail({ to, subject, html })` — nothing more
35
- 2. **Non-Blocking**: Email failures logged but never thrown — don't crash user flows
36
- 3. **Provider-Agnostic**: Wrap any email API behind the same interface
37
- 4. **Template Builders**: Separate functions build HTML content
38
- 5. **Server-Side Only**: Email sending only from API routes, cron jobs, and server functions
39
- 6. **Secrets via Environment**: API keys stored as Cloudflare secrets, never hardcoded
40
-
41
- ---
42
-
43
- ## Implementation
44
-
45
- ### Structure
46
-
47
- ```
48
- src/lib/
49
- ├── email/
50
- │ ├── send-email.ts # Core send function
51
- │ ├── templates/
52
- │ │ ├── base.ts # Base HTML wrapper
53
- │ │ ├── welcome.ts # Welcome email template
54
- │ │ ├── daily-digest.ts # Daily digest template
55
- │ │ ├── appointment.ts # Appointment notification
56
- │ │ └── password-reset.ts # Password reset template
57
- │ └── index.ts # Barrel export
58
- ```
59
-
60
- ### Code Example
61
-
62
- #### Step 1: Core Send Function
63
-
64
- ```typescript
65
- // src/lib/email/send-email.ts
66
-
67
- const MANDRILL_API_URL = 'https://mandrillapp.com/api/1.0/messages/send'
68
-
69
- interface SendEmailParams {
70
- to: string | string[]
71
- subject: string
72
- html: string
73
- from?: string
74
- fromName?: string
75
- }
76
-
77
- interface SendEmailResult {
78
- success: boolean
79
- error?: string
80
- }
81
-
82
- /**
83
- * Send a transactional email via Mandrill.
84
- * Non-blocking: logs errors but never throws.
85
- */
86
- export async function sendEmail(params: SendEmailParams): Promise<SendEmailResult> {
87
- const apiKey = process.env.MANDRILL_API_KEY
88
-
89
- if (!apiKey) {
90
- console.error('[Email] Mandrill API key not configured')
91
- return { success: false, error: 'API key not configured' }
92
- }
93
-
94
- const recipients = Array.isArray(params.to)
95
- ? params.to.map(email => ({ email, type: 'to' as const }))
96
- : [{ email: params.to, type: 'to' as const }]
97
-
98
- try {
99
- const response = await fetch(MANDRILL_API_URL, {
100
- method: 'POST',
101
- headers: { 'Content-Type': 'application/json' },
102
- body: JSON.stringify({
103
- key: apiKey,
104
- message: {
105
- html: params.html,
106
- subject: params.subject,
107
- from_email: params.from || 'noreply@example.com',
108
- from_name: params.fromName || 'My App',
109
- to: recipients,
110
- important: true,
111
- track_opens: true,
112
- track_clicks: true,
113
- auto_text: true,
114
- },
115
- }),
116
- })
117
-
118
- if (!response.ok) {
119
- const error = await response.text()
120
- console.error('[Email] Send failed:', error)
121
- return { success: false, error }
122
- }
123
-
124
- const data = await response.json()
125
- console.log(`[Email] Sent "${params.subject}" to ${recipients.length} recipient(s)`)
126
- return { success: true }
127
- } catch (error) {
128
- console.error('[Email] Send error:', error)
129
- return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }
130
- }
131
- }
132
- ```
133
-
134
- #### Step 2: Base HTML Template
135
-
136
- ```typescript
137
- // src/lib/email/templates/base.ts
138
-
139
- interface BaseTemplateParams {
140
- title: string
141
- body: string
142
- footerText?: string
143
- }
144
-
145
- /**
146
- * Base HTML email wrapper with consistent styling
147
- */
148
- export function baseTemplate({ title, body, footerText }: BaseTemplateParams): string {
149
- return `<!DOCTYPE html>
150
- <html>
151
- <head>
152
- <meta charset="utf-8">
153
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
154
- <title>${title}</title>
155
- <style>
156
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 0; background: #f5f5f5; }
157
- .container { max-width: 600px; margin: 0 auto; padding: 20px; }
158
- .card { background: #ffffff; border-radius: 8px; padding: 32px; margin: 20px 0; }
159
- .header { text-align: center; padding-bottom: 20px; border-bottom: 1px solid #eee; margin-bottom: 20px; }
160
- .footer { text-align: center; color: #888; font-size: 12px; padding: 20px 0; }
161
- h1 { color: #333; font-size: 24px; margin: 0; }
162
- p { color: #555; line-height: 1.6; }
163
- .btn { display: inline-block; background: #3b82f6; color: #fff; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 600; }
164
- </style>
165
- </head>
166
- <body>
167
- <div class="container">
168
- <div class="card">
169
- <div class="header">
170
- <h1>${title}</h1>
171
- </div>
172
- ${body}
173
- </div>
174
- <div class="footer">
175
- ${footerText || 'You are receiving this because you have an account with us.'}
176
- </div>
177
- </div>
178
- </body>
179
- </html>`
180
- }
181
- ```
182
-
183
- #### Step 3: Domain-Specific Template
184
-
185
- ```typescript
186
- // src/lib/email/templates/daily-digest.ts
187
-
188
- import { baseTemplate } from './base'
189
-
190
- interface DigestData {
191
- checkIns: { guestName: string; property: string }[]
192
- checkOuts: { guestName: string; property: string }[]
193
- unclaimedCleans: { property: string; date: string }[]
194
- }
195
-
196
- export function dailyDigestTemplate(data: DigestData): string {
197
- const sections: string[] = []
198
-
199
- if (data.checkIns.length > 0) {
200
- sections.push(`
201
- <h2>Check-Ins Today (${data.checkIns.length})</h2>
202
- <ul>
203
- ${data.checkIns.map(c => `<li><strong>${c.guestName}</strong> at ${c.property}</li>`).join('')}
204
- </ul>
205
- `)
206
- }
207
-
208
- if (data.checkOuts.length > 0) {
209
- sections.push(`
210
- <h2>Check-Outs Today (${data.checkOuts.length})</h2>
211
- <ul>
212
- ${data.checkOuts.map(c => `<li><strong>${c.guestName}</strong> at ${c.property}</li>`).join('')}
213
- </ul>
214
- `)
215
- }
216
-
217
- if (data.unclaimedCleans.length > 0) {
218
- sections.push(`
219
- <h2>Unclaimed Cleans (${data.unclaimedCleans.length})</h2>
220
- <ul>
221
- ${data.unclaimedCleans.map(c => `<li>${c.property} — ${c.date}</li>`).join('')}
222
- </ul>
223
- `)
224
- }
225
-
226
- if (sections.length === 0) {
227
- sections.push('<p>No activity to report today.</p>')
228
- }
229
-
230
- return baseTemplate({
231
- title: `Daily Digest — ${new Date().toLocaleDateString()}`,
232
- body: sections.join(''),
233
- })
234
- }
235
- ```
236
-
237
- #### Step 4: Use in Application Code
238
-
239
- ```typescript
240
- // src/lib/scheduled/daily-digest.ts
241
-
242
- import { sendEmail } from '@/lib/email'
243
- import { dailyDigestTemplate } from '@/lib/email/templates/daily-digest'
244
-
245
- export async function handleDailyDigest(): Promise<void> {
246
- const checkIns = await ReservationDatabaseService.getTodayCheckIns()
247
- const checkOuts = await ReservationDatabaseService.getTodayCheckOuts()
248
- const unclaimedCleans = await AppointmentDatabaseService.getUnclaimedNextWeek()
249
-
250
- const html = dailyDigestTemplate({ checkIns, checkOuts, unclaimedCleans })
251
-
252
- await sendEmail({
253
- to: ['manager@example.com', 'ops@example.com'],
254
- subject: `Daily Digest — ${new Date().toLocaleDateString()}`,
255
- html,
256
- })
257
- }
258
- ```
259
-
260
- #### Step 5: Use in API Route (Non-Blocking)
261
-
262
- ```typescript
263
- // routes/api/posts/create.tsx
264
-
265
- POST: async ({ request }) => {
266
- const user = await getAuthSession()
267
- const body = await request.json()
268
- const post = await PostDatabaseService.create(user.uid, body)
269
-
270
- // Send notification email (non-blocking)
271
- sendEmail({
272
- to: 'admin@example.com',
273
- subject: `New post: ${body.title}`,
274
- html: baseTemplate({
275
- title: 'New Post Created',
276
- body: `<p>${user.displayName} created a new post: <strong>${body.title}</strong></p>`,
277
- }),
278
- }).catch(error => console.error('[Email] Notification failed:', error))
279
- // Note: not awaited — fire and forget
280
-
281
- return new Response(JSON.stringify(post), {
282
- status: 201,
283
- headers: { 'Content-Type': 'application/json' },
284
- })
285
- }
286
- ```
287
-
288
- ---
289
-
290
- ## Swapping Providers
291
-
292
- The `sendEmail` function wraps a single provider. To swap providers, change only that file:
293
-
294
- ### Mandrill → SendGrid
295
-
296
- ```typescript
297
- // src/lib/email/send-email.ts (SendGrid version)
298
- export async function sendEmail(params: SendEmailParams): Promise<SendEmailResult> {
299
- const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
300
- method: 'POST',
301
- headers: {
302
- 'Authorization': `Bearer ${process.env.SENDGRID_API_KEY}`,
303
- 'Content-Type': 'application/json',
304
- },
305
- body: JSON.stringify({
306
- personalizations: [{ to: [{ email: params.to }] }],
307
- from: { email: params.from || 'noreply@example.com' },
308
- subject: params.subject,
309
- content: [{ type: 'text/html', value: params.html }],
310
- }),
311
- })
312
- // ...
313
- }
314
- ```
315
-
316
- ### Mandrill → Resend
317
-
318
- ```typescript
319
- // src/lib/email/send-email.ts (Resend version)
320
- export async function sendEmail(params: SendEmailParams): Promise<SendEmailResult> {
321
- const response = await fetch('https://api.resend.com/emails', {
322
- method: 'POST',
323
- headers: {
324
- 'Authorization': `Bearer ${process.env.RESEND_API_KEY}`,
325
- 'Content-Type': 'application/json',
326
- },
327
- body: JSON.stringify({
328
- from: params.from || 'noreply@example.com',
329
- to: params.to,
330
- subject: params.subject,
331
- html: params.html,
332
- }),
333
- })
334
- // ...
335
- }
336
- ```
337
-
338
- ---
339
-
340
- ## Benefits
341
-
342
- ### 1. Simple Interface
343
- `sendEmail({ to, subject, html })` — no provider-specific knowledge needed in calling code.
344
-
345
- ### 2. Non-Blocking
346
- Email failures never crash user operations. Fire-and-forget pattern for notifications.
347
-
348
- ### 3. Provider Swappable
349
- Change email provider by editing one file. No impact on templates or callers.
350
-
351
- ### 4. Testable
352
- `sendEmail` is easily mockable for testing. Templates are pure functions returning strings.
353
-
354
- ---
355
-
356
- ## Trade-offs
357
-
358
- ### 1. HTML Templates as Strings
359
- **Downside**: Building HTML in template literals is error-prone and hard to preview.
360
- **Mitigation**: Use the base template for consistent structure. Test templates by rendering in a browser.
361
-
362
- ### 2. No MJML/React Email
363
- **Downside**: Not using modern email frameworks (MJML, React Email).
364
- **Mitigation**: These can be added later. The `sendEmail` interface stays the same — only template builders change.
365
-
366
- ---
367
-
368
- ## Anti-Patterns
369
-
370
- ### ❌ Anti-Pattern: Throwing on Email Failure
371
-
372
- ```typescript
373
- // ❌ BAD: Email failure crashes the post creation
374
- const post = await PostService.create(data)
375
- await sendEmail({ to, subject, html }) // If this throws, post appears to fail!
376
- return post
377
-
378
- // ✅ GOOD: Fire and forget
379
- const post = await PostService.create(data)
380
- sendEmail({ to, subject, html }).catch(err => console.error('[Email]', err))
381
- return post
382
- ```
383
-
384
- ### ❌ Anti-Pattern: Inline HTML in Route Handlers
385
-
386
- ```typescript
387
- // ❌ BAD: HTML template inline in route
388
- await sendEmail({
389
- to: user.email,
390
- subject: 'Welcome',
391
- html: `<html><body><h1>Welcome ${user.name}!</h1><p>...</p></body></html>`
392
- })
393
-
394
- // ✅ GOOD: Use template function
395
- import { welcomeTemplate } from '@/lib/email/templates/welcome'
396
- await sendEmail({
397
- to: user.email,
398
- subject: 'Welcome',
399
- html: welcomeTemplate({ name: user.name })
400
- })
401
- ```
402
-
403
- ---
404
-
405
- ## Related Patterns
406
-
407
- - **[Scheduled Tasks](./tanstack-cloudflare.scheduled-tasks.md)**: Cron-triggered emails (daily digests, reminders)
408
- - **[Third-Party API Integration](./tanstack-cloudflare.third-party-api-integration.md)**: Email provider as an integration
409
- - **[API Route Handlers](./tanstack-cloudflare.api-route-handlers.md)**: Email sent from API routes
410
-
411
- ---
412
-
413
- ## Checklist for Implementation
414
-
415
- - [ ] `sendEmail()` function with `{ to, subject, html }` interface
416
- - [ ] Returns `{ success, error? }` — never throws
417
- - [ ] API key stored as Cloudflare secret
418
- - [ ] Base HTML template with consistent styling
419
- - [ ] Domain-specific template functions (digest, notification, etc.)
420
- - [ ] Templates are pure functions (string input → string output)
421
- - [ ] Non-critical emails sent fire-and-forget (`.catch()`)
422
- - [ ] Critical emails awaited but wrapped in try/catch
423
- - [ ] Multiple recipients supported (string or string array)
424
- - [ ] Provider can be swapped by editing one file
425
-
426
- ---
427
-
428
- **Status**: Stable - Production-ready email service for Cloudflare Workers
429
- **Recommendation**: Use for all transactional email needs
430
- **Last Updated**: 2026-02-28
431
- **Contributors**: Patrick Michaelsen
@@ -1,98 +0,0 @@
1
- # Expander Component
2
-
3
- **Category**: Design
4
- **Applicable To**: Expandable/collapsible sections with smooth height animation and 10 visual variants
5
- **Status**: Stable
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- A polymorphic expandable section component with 10 distinct visual variants (gradient-glow, neon-accent, glass-float, slide-arrow, border-sweep, stacked-lift, visibility, thread, highlight, segmented). Uses a `useCollapse` hook for CSS-transition-based height animation (300ms cubic-bezier). Controlled open/close state with consistent API across all variants.
12
-
13
- ---
14
-
15
- ## Implementation
16
-
17
- **File**: `src/components/Expander.tsx`
18
-
19
- ```typescript
20
- interface ExpanderProps {
21
- title: string
22
- count?: number // Optional count badge (border-sweep variant)
23
- open: boolean // Controlled state
24
- onToggle: () => void
25
- children: ReactNode
26
- }
27
-
28
- function Expander({ variant = 'gradient-glow', ...props }: ExpanderProps & { variant?: ExpanderVariant })
29
- ```
30
-
31
- ### useCollapse Hook (Height Animation)
32
-
33
- ```typescript
34
- function useCollapse(open: boolean) {
35
- const ref = useRef<HTMLDivElement>(null)
36
- const [height, setHeight] = useState<number | undefined>(open ? undefined : 0)
37
-
38
- useEffect(() => {
39
- const el = ref.current
40
- if (!el) return
41
- if (open) {
42
- setHeight(el.scrollHeight)
43
- const id = setTimeout(() => setHeight(undefined), 300) // Auto after animation
44
- return () => clearTimeout(id)
45
- } else {
46
- setHeight(el.scrollHeight)
47
- requestAnimationFrame(() => requestAnimationFrame(() => setHeight(0)))
48
- }
49
- }, [open])
50
-
51
- return {
52
- ref,
53
- style: {
54
- height: height != null ? `${height}px` : 'auto',
55
- overflow: 'hidden' as const,
56
- transition: 'height 300ms cubic-bezier(0.4, 0, 0.2, 1)',
57
- },
58
- }
59
- }
60
- ```
61
-
62
- ### Variants
63
-
64
- | Variant | Visual |
65
- |---|---|
66
- | `gradient-glow` | Blue text title with chevron, minimal |
67
- | `neon-accent` | Left green border (glows on open), plus/minus icon |
68
- | `glass-float` | Frosted glass blur effect when open, sparkle icon |
69
- | `slide-arrow` | Arrow rotates 90° on open, indented content |
70
- | `border-sweep` | Bottom amber gradient border sweeps in, count badge |
71
- | `stacked-lift` | Layered border effects above title when open |
72
- | `visibility` | iOS-style toggle switch, eye/eye-off icon |
73
- | `thread` | Gradient vertical line with nested indentation |
74
- | `highlight` | Indigo ring + background highlight, zap icon with scale |
75
- | `segmented` | 3 animated dots (staggered timing), fuchsia color |
76
-
77
- Exported as `EXPANDER_VARIANTS: Array<{ id: string; label: string }>`.
78
-
79
- **Usage**:
80
-
81
- ```typescript
82
- const [open, setOpen] = useState(false)
83
-
84
- <Expander
85
- variant="glass-float"
86
- title="Advanced Settings"
87
- open={open}
88
- onToggle={() => setOpen(!open)}
89
- >
90
- <p>Expanded content here</p>
91
- </Expander>
92
- ```
93
-
94
- ---
95
-
96
- **Status**: Stable
97
- **Last Updated**: 2026-03-14
98
- **Contributors**: Community
@@ -1,115 +0,0 @@
1
- # FCM Push Notifications
2
-
3
- **Category**: Architecture
4
- **Applicable To**: Multi-device push notifications via Firebase Cloud Messaging with automatic stale token cleanup
5
- **Status**: Stable
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- Server-side FCM push notification delivery to all registered devices for a user. Tokens stored in Firestore per-user with hash-based document IDs for upsert. Invalid/expired tokens (`UNREGISTERED`, `INVALID_ARGUMENT`) are automatically removed on send failure. Integrated with the notification triggers service as the offline fallback when WebSocket is unavailable.
12
-
13
- ---
14
-
15
- ## Implementation
16
-
17
- ### FcmService
18
-
19
- **File**: `src/services/fcm.service.ts`
20
-
21
- ```typescript
22
- interface PushNotificationPayload {
23
- title: string
24
- body: string
25
- data?: Record<string, string>
26
- }
27
-
28
- class FcmService {
29
- static async sendToUser(userId: string, payload: PushNotificationPayload): Promise<number> {
30
- const tokens = await FcmTokenDatabaseService.getTokens(userId)
31
- if (tokens.length === 0) return 0
32
-
33
- let successCount = 0
34
- await Promise.all(tokens.map(async (token) => {
35
- try {
36
- await sendMessage({
37
- token: token.fcm_token,
38
- notification: { title: payload.title, body: payload.body },
39
- data: payload.data,
40
- })
41
- successCount++
42
- } catch (error) {
43
- if (errMsg.includes('UNREGISTERED') || errMsg.includes('INVALID_ARGUMENT')) {
44
- await FcmTokenDatabaseService.removeTokenById(userId, token.id) // Auto-cleanup
45
- }
46
- }
47
- }))
48
- return successCount
49
- }
50
- }
51
- ```
52
-
53
- ### FcmTokenDatabaseService
54
-
55
- **File**: `src/services/fcm-token-database.service.ts`
56
-
57
- ```typescript
58
- interface FcmToken {
59
- id: string // Hash of fcm_token
60
- fcm_token: string
61
- platform: 'ios' | 'android' | 'web'
62
- created_at: string
63
- updated_at: string
64
- }
65
- // Collection: users/{userId}/fcm_tokens
66
- // Document ID: hashToken(fcmToken)
67
-
68
- class FcmTokenDatabaseService {
69
- static async upsertToken(userId, fcmToken, platform): Promise<FcmToken>
70
- static async removeToken(userId, fcmToken): Promise<void>
71
- static async removeTokenById(userId, tokenId): Promise<void>
72
- static async getTokens(userId): Promise<FcmToken[]>
73
- }
74
- ```
75
-
76
- ### Client Registration
77
-
78
- ```typescript
79
- // POST /api/mobile/register-fcm-token
80
- // Body: { token: string, platform: 'ios' | 'android' | 'web' }
81
- await FcmTokenDatabaseService.upsertToken(userId, token, platform)
82
- ```
83
-
84
- ### Delivery Strategy (with NotificationTriggers)
85
-
86
- ```typescript
87
- // WebSocket-first, FCM-fallback
88
- if (await NotificationHubService.isUserConnected(env, recipientId)) {
89
- await NotificationHubService.pushNotification(env, recipientId, notification)
90
- } else {
91
- await FcmService.sendToUser(recipientId, { title, body, data })
92
- }
93
- ```
94
-
95
- ---
96
-
97
- ## Checklist
98
-
99
- - [ ] Token doc ID is hash of FCM token (enables upsert without duplicates)
100
- - [ ] `sendToUser` sends to ALL registered tokens (multiple devices)
101
- - [ ] Invalid tokens auto-removed on UNREGISTERED/INVALID_ARGUMENT errors
102
- - [ ] Client registers token on app launch / permission grant
103
- - [ ] Delivery strategy checks WebSocket first, falls back to FCM
104
-
105
- ---
106
-
107
- ## Related Patterns
108
-
109
- - **[Notifications Engine](./tanstack-cloudflare.notifications-engine.md)**: WebSocket-first delivery that FCM falls back from
110
-
111
- ---
112
-
113
- **Status**: Stable
114
- **Last Updated**: 2026-03-14
115
- **Contributors**: Community