@jhits/plugin-newsletter 0.0.6 → 0.0.8

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 (45) hide show
  1. package/package.json +8 -9
  2. package/src/api/handler.ts +0 -693
  3. package/src/api/router.ts +0 -111
  4. package/src/index.server.ts +0 -12
  5. package/src/index.tsx +0 -313
  6. package/src/index.tsx.patch +0 -98
  7. package/src/init.tsx +0 -72
  8. package/src/lib/blocks/BlockRenderer.tsx +0 -125
  9. package/src/lib/email/EmailRenderer.tsx +0 -425
  10. package/src/lib/email/index.ts +0 -6
  11. package/src/lib/mappers/apiMapper.ts +0 -57
  12. package/src/lib/utils/blockHelpers.ts +0 -71
  13. package/src/lib/utils/slugify.ts +0 -43
  14. package/src/registry/BlockRegistry.ts +0 -53
  15. package/src/registry/index.ts +0 -5
  16. package/src/state/EditorContext.tsx +0 -279
  17. package/src/state/index.ts +0 -10
  18. package/src/state/reducer.ts +0 -561
  19. package/src/state/types.ts +0 -154
  20. package/src/types/block.ts +0 -275
  21. package/src/types/newsletter.ts +0 -151
  22. package/src/types/registry.ts +0 -14
  23. package/src/views/CanvasEditor/BlockWrapper.tsx +0 -143
  24. package/src/views/CanvasEditor/CanvasEditorView.tsx +0 -249
  25. package/src/views/CanvasEditor/EditorBody.tsx +0 -95
  26. package/src/views/CanvasEditor/EditorHeader.tsx +0 -139
  27. package/src/views/CanvasEditor/components/CustomBlockItem.tsx +0 -83
  28. package/src/views/CanvasEditor/components/EditorCanvas.tsx +0 -674
  29. package/src/views/CanvasEditor/components/EditorLibrary.tsx +0 -120
  30. package/src/views/CanvasEditor/components/EditorSidebar.tsx +0 -156
  31. package/src/views/CanvasEditor/components/ErrorBanner.tsx +0 -31
  32. package/src/views/CanvasEditor/components/LibraryItem.tsx +0 -71
  33. package/src/views/CanvasEditor/components/SlashCommandDetector.tsx +0 -196
  34. package/src/views/CanvasEditor/components/SlashCommandMenu.tsx +0 -131
  35. package/src/views/CanvasEditor/components/index.ts +0 -16
  36. package/src/views/CanvasEditor/hooks/index.ts +0 -7
  37. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +0 -136
  38. package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +0 -34
  39. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +0 -54
  40. package/src/views/CanvasEditor/hooks/useSlashCommand.ts +0 -106
  41. package/src/views/CanvasEditor/index.ts +0 -12
  42. package/src/views/NewsletterEditor.tsx +0 -38
  43. package/src/views/NewsletterManager.tsx +0 -240
  44. package/src/views/SettingsView.tsx +0 -216
  45. package/src/views/SubscribersView.tsx +0 -269
@@ -1,125 +0,0 @@
1
- /**
2
- * Block Renderer
3
- * Library component for rendering blocks (decoupled from editor)
4
- * This is the "headless" rendering layer
5
- *
6
- * Multi-Tenant: Uses Preview components from client-provided blocks
7
- */
8
-
9
- 'use client';
10
-
11
- import React from 'react';
12
- import { Block, BlockPreviewProps } from '../../types/block';
13
- import { blockRegistry } from '../../registry/BlockRegistry';
14
- import { getChildBlocks } from '../utils/blockHelpers';
15
-
16
- /**
17
- * Block Renderer Props
18
- */
19
- export interface BlockRendererProps {
20
- /** Block to render */
21
- block: Block;
22
-
23
- /** Custom renderers for specific block types (optional override) */
24
- customRenderers?: Map<string, React.ComponentType<BlockPreviewProps>>;
25
-
26
- /** Additional context for rendering */
27
- context?: {
28
- siteId?: string;
29
- locale?: string;
30
- [key: string]: unknown;
31
- };
32
- }
33
-
34
- /**
35
- * Block Renderer Component
36
- * Renders a single block using its Preview component from the registry
37
- */
38
- export function BlockRenderer({
39
- block,
40
- customRenderers,
41
- context = {}
42
- }: BlockRendererProps) {
43
- // Check for custom renderer override first
44
- if (customRenderers?.has(block.type)) {
45
- const CustomRenderer = customRenderers.get(block.type)!;
46
- return <CustomRenderer block={block} context={context} />;
47
- }
48
-
49
- // Get block definition from registry
50
- const definition = blockRegistry.get(block.type);
51
- if (!definition) {
52
- console.warn(`Block type "${block.type}" not found in registry. Available types:`,
53
- blockRegistry.getAll().map(b => b.type).join(', '));
54
- return (
55
- <div className="p-4 border border-red-300 bg-red-50 rounded">
56
- <p className="text-red-600">Unknown block type: {block.type}</p>
57
- <p className="text-xs text-red-500 mt-1">
58
- Make sure this block type is registered via customBlocks prop
59
- </p>
60
- </div>
61
- );
62
- }
63
-
64
- // Use the Preview component from the block definition
65
- const PreviewComponent = definition.components.Preview;
66
-
67
- // Check if this is a container block with children
68
- const isContainer = definition.isContainer === true;
69
- const childBlocks = isContainer && block.children && Array.isArray(block.children) && block.children.length > 0
70
- ? (typeof block.children[0] === 'object'
71
- ? block.children as Block[]
72
- : [])
73
- : [];
74
-
75
- // If container block, pass child blocks and render function
76
- if (isContainer) {
77
- return (
78
- <PreviewComponent
79
- block={block}
80
- context={context}
81
- childBlocks={childBlocks}
82
- renderChild={(childBlock: Block) => (
83
- <BlockRenderer
84
- block={childBlock}
85
- customRenderers={customRenderers}
86
- context={context}
87
- />
88
- )}
89
- />
90
- );
91
- }
92
-
93
- return <PreviewComponent block={block} context={context} />;
94
- }
95
-
96
- /**
97
- * Blocks Renderer Component
98
- * Renders multiple blocks in sequence
99
- */
100
- export function BlocksRenderer({
101
- blocks,
102
- customRenderers,
103
- context = {}
104
- }: {
105
- blocks: Block[];
106
- customRenderers?: Map<string, React.ComponentType<BlockPreviewProps>>;
107
- context?: {
108
- siteId?: string;
109
- locale?: string;
110
- [key: string]: unknown;
111
- };
112
- }) {
113
- return (
114
- <>
115
- {blocks.map((block) => (
116
- <BlockRenderer
117
- key={block.id}
118
- block={block}
119
- customRenderers={customRenderers}
120
- context={context}
121
- />
122
- ))}
123
- </>
124
- );
125
- }
@@ -1,425 +0,0 @@
1
- /**
2
- * Email Renderer
3
- * Converts blocks to email-safe HTML
4
- *
5
- * Email constraints:
6
- * - Inline styles only (no CSS classes)
7
- * - Table-based layouts (no flexbox/grid)
8
- * - Max width ~600px
9
- * - Limited CSS support
10
- * - Images need absolute URLs
11
- * - No JavaScript
12
- */
13
-
14
- import { Block } from '../../types/block';
15
- import { blockRegistry } from '../../registry/BlockRegistry';
16
- import { getChildBlocks } from '../utils/blockHelpers';
17
-
18
- /**
19
- * Email rendering context
20
- */
21
- export interface EmailRenderContext {
22
- siteId?: string;
23
- locale?: string;
24
- baseUrl?: string; // Base URL for images and links
25
- logoUrl?: string; // Logo URL for email header
26
- logoAlt?: string; // Logo alt text
27
- [key: string]: unknown;
28
- }
29
-
30
- /**
31
- * Convert a block to email-safe HTML
32
- */
33
- export function renderBlockToEmail(block: Block, context: EmailRenderContext = {}): string {
34
- const definition = blockRegistry.get(block.type);
35
-
36
- if (!definition) {
37
- return `<p style="color: #666; font-size: 14px; padding: 10px;">Unknown block type: ${block.type}</p>`;
38
- }
39
-
40
- // Check if block has email-specific renderer (preferred)
41
- if (definition.components.Email) {
42
- const childBlocks = block.children && Array.isArray(block.children) && block.children.length > 0
43
- ? (typeof block.children[0] === 'object' ? block.children as Block[] : [])
44
- : [];
45
-
46
- return definition.components.Email({
47
- block,
48
- context,
49
- childBlocks,
50
- renderChild: (childBlock: Block) => renderBlockToEmail(childBlock, context),
51
- });
52
- }
53
-
54
- // Fallback to converting Preview component output to email HTML
55
- // For now, we'll create basic email-safe HTML based on block type
56
- return renderBlockByType(block, context);
57
- }
58
-
59
- /**
60
- * Render block by type with email-safe HTML
61
- */
62
- function renderBlockByType(block: Block, context: EmailRenderContext): string {
63
- const { baseUrl = '' } = context;
64
-
65
- switch (block.type) {
66
- case 'heading': {
67
- const level = (block.data.level as number) || 1;
68
- const text = (block.data.text as string) || '';
69
- const tag = `h${Math.min(Math.max(level, 1), 6)}`;
70
- const fontSize = level === 1 ? '32px' : level === 2 ? '24px' : level === 3 ? '20px' : '18px';
71
- return `<${tag} style="font-size: ${fontSize}; font-weight: bold; color: #1a2e26; margin: 20px 0 10px 0; line-height: 1.3;">${escapeHtml(text)}</${tag}>`;
72
- }
73
-
74
- case 'paragraph': {
75
- const html = (block.data.html as string) || (block.data.text as string) || '';
76
- // Convert basic HTML tags to email-safe inline styles
77
- const emailHtml = convertHtmlToEmailSafe(html);
78
- return `<p style="font-size: 15px; line-height: 1.8; color: #1a2e26; margin: 0 0 15px 0;">${emailHtml}</p>`;
79
- }
80
-
81
- case 'image': {
82
- const imageId = block.data.imageId as string;
83
- const alt = (block.data.alt as string) || '';
84
- const caption = (block.data.caption as string) || '';
85
- const height = (block.data.height as number) || undefined;
86
- const widthPercent = (block.data.widthPercent as number) || 100;
87
- const brightness = (block.data.brightness as number) ?? 100;
88
- const blur = (block.data.blur as number) ?? 0;
89
- const borderRadius = (block.data.borderRadius as string) || 'none';
90
-
91
- if (!imageId) {
92
- return '';
93
- }
94
-
95
- // Check if imageId is already a filename (has extension or starts with timestamp)
96
- const hasFileExtension = /\.(jpg|jpeg|png|webp|gif|svg)$/i.test(imageId);
97
- const looksLikeTimestamp = /^\d+-/.test(imageId);
98
- const isFilename = hasFileExtension || looksLikeTimestamp;
99
-
100
- // Generate image URL
101
- const imageUrl = isFilename
102
- ? `${baseUrl}/api/uploads/${encodeURIComponent(imageId)}`
103
- : `${baseUrl}/api/uploads/${encodeURIComponent(imageId)}`; // Will be resolved in preview component
104
-
105
- // Build image styles
106
- const imageStyles: string[] = [
107
- 'display: block',
108
- 'max-width: 100%',
109
- ];
110
-
111
- // Apply width percentage
112
- if (widthPercent !== 100) {
113
- imageStyles.push(`width: ${widthPercent}%`);
114
- }
115
-
116
- // Apply height if specified
117
- if (height) {
118
- imageStyles.push(`height: ${height}px`);
119
- imageStyles.push('object-fit: cover');
120
- } else {
121
- imageStyles.push('height: auto');
122
- }
123
-
124
- // Apply filters (brightness and blur) - supported in most email clients
125
- const filters: string[] = [];
126
- if (brightness !== 100) {
127
- filters.push(`brightness(${brightness}%)`);
128
- }
129
- if (blur > 0) {
130
- filters.push(`blur(${blur}px)`);
131
- }
132
- if (filters.length > 0) {
133
- imageStyles.push(`filter: ${filters.join(' ')}`);
134
- }
135
-
136
- // Apply border radius
137
- const borderRadiusMap: Record<string, string> = {
138
- 'none': '0',
139
- 'sm': '2px',
140
- 'md': '4px',
141
- 'lg': '8px',
142
- 'xl': '12px',
143
- '2xl': '16px',
144
- '3xl': '24px',
145
- 'full': '9999px',
146
- };
147
- const borderRadiusValue = borderRadiusMap[borderRadius] || '0';
148
- if (borderRadiusValue !== '0') {
149
- imageStyles.push(`border-radius: ${borderRadiusValue}`);
150
- }
151
-
152
- const imageStyleString = imageStyles.join('; ');
153
-
154
- let html = `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 20px 0;">
155
- <tr>
156
- <td align="center">
157
- <img src="${imageUrl}" alt="${escapeHtml(alt)}" style="${imageStyleString}" />
158
- </td>
159
- </tr>`;
160
-
161
- if (caption) {
162
- html += `<tr>
163
- <td align="center" style="padding-top: 10px; font-size: 12px; color: #666; font-style: italic;">
164
- ${escapeHtml(caption)}
165
- </td>
166
- </tr>`;
167
- }
168
-
169
- html += `</table>`;
170
- return html;
171
- }
172
-
173
- case 'list': {
174
- const items = (block.data.items as Array<string | { text: string; html?: string }>) || [];
175
- const type = (block.data.type as string) || 'ul';
176
- const isOrdered = type === 'ol';
177
- const tag = isOrdered ? 'ol' : 'ul';
178
-
179
- const itemsHtml = items.map((item, idx) => {
180
- const text = typeof item === 'string' ? item : (item.html || item.text || '');
181
- const emailHtml = convertHtmlToEmailSafe(text);
182
- return `<li style="margin: 8px 0; padding-left: 5px; line-height: 1.8; color: #1a2e26;">${emailHtml}</li>`;
183
- }).join('');
184
-
185
- return `<${tag} style="margin: 15px 0; padding-left: 25px; color: #1a2e26; font-size: 15px; line-height: 1.8;">${itemsHtml}</${tag}>`;
186
- }
187
-
188
- case 'table': {
189
- const rows = (Array.isArray(block.data?.rows) ? block.data.rows : []) as Array<Array<{ text: string; html: string }>>;
190
- const useHeader = block.data.useHeader ?? true;
191
-
192
- if (!rows.length) {
193
- return '';
194
- }
195
-
196
- // Build table HTML using email-safe table structure
197
- let tableHtml = `<table width="100%" cellpadding="12" cellspacing="0" border="0" style="margin: 20px 0; border-collapse: collapse; width: 100%;">`;
198
-
199
- rows.forEach((row, rIdx) => {
200
- const isHeaderRow = rIdx === 0 && useHeader;
201
- const tag = isHeaderRow ? 'th' : 'td';
202
- const bgColor = isHeaderRow ? '#1a2e26' : 'transparent';
203
- const textColor = isHeaderRow ? '#ffffff' : '#1a2e26';
204
- const fontWeight = isHeaderRow ? 'bold' : 'normal';
205
-
206
- tableHtml += `<tr>`;
207
- row.forEach((cell) => {
208
- const cellHtml = convertHtmlToEmailSafe(cell.html || cell.text || '');
209
- tableHtml += `<${tag} style="padding: 12px; border: 1px solid #e5e7eb; text-align: left; color: ${textColor}; font-size: 15px; line-height: 1.8; background-color: ${bgColor}; font-weight: ${fontWeight};">${cellHtml}</${tag}>`;
210
- });
211
- tableHtml += `</tr>`;
212
- });
213
-
214
- tableHtml += `</table>`;
215
- return tableHtml;
216
- }
217
-
218
- case 'divider': {
219
- return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 30px 0;">
220
- <tr>
221
- <td align="center">
222
- <table width="40" cellpadding="0" cellspacing="0" border="0">
223
- <tr>
224
- <td height="1" bgcolor="#1a2e26" style="background-color: #1a2e2620; height: 1px;"></td>
225
- </tr>
226
- </table>
227
- </td>
228
- </tr>
229
- </table>`;
230
- }
231
-
232
- default:
233
- // Generic fallback - try to extract text content
234
- const text = extractTextFromBlock(block);
235
- if (text) {
236
- return `<p style="font-size: 15px; line-height: 1.8; color: #1a2e26; margin: 0 0 15px 0;">${escapeHtml(text)}</p>`;
237
- }
238
- return '';
239
- }
240
- }
241
-
242
- /**
243
- * Convert HTML to email-safe HTML (inline styles)
244
- */
245
- function convertHtmlToEmailSafe(html: string): string {
246
- if (!html) return '';
247
-
248
- // Convert common HTML tags to email-safe versions with inline styles
249
- let result = html
250
- // Bold
251
- .replace(/<strong\b[^>]*>(.*?)<\/strong>/gi, '<strong style="font-weight: bold;">$1</strong>')
252
- .replace(/<b\b[^>]*>(.*?)<\/b>/gi, '<strong style="font-weight: bold;">$1</strong>')
253
- // Italic
254
- .replace(/<em\b[^>]*>(.*?)<\/em>/gi, '<em style="font-style: italic;">$1</em>')
255
- .replace(/<i\b[^>]*>(.*?)<\/i>/gi, '<em style="font-style: italic;">$1</em>')
256
- // Links
257
- .replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi, '<a href="$1" style="color: #1a2e26; text-decoration: underline;">$2</a>')
258
- // Line breaks
259
- .replace(/<br\s*\/?>/gi, '<br />');
260
-
261
- return result;
262
- }
263
-
264
- /**
265
- * Extract text content from block data
266
- */
267
- function extractTextFromBlock(block: Block): string {
268
- if (block.data.text) return String(block.data.text);
269
- if (block.data.html) {
270
- const html = String(block.data.html);
271
- return html.replace(/<[^>]*>/g, '').trim();
272
- }
273
- if (block.data.title) return String(block.data.title);
274
- return '';
275
- }
276
-
277
- /**
278
- * Escape HTML entities
279
- */
280
- function escapeHtml(text: string): string {
281
- const map: Record<string, string> = {
282
- '&': '&amp;',
283
- '<': '&lt;',
284
- '>': '&gt;',
285
- '"': '&quot;',
286
- "'": '&#039;',
287
- };
288
- return text.replace(/[&<>"']/g, (m) => map[m]);
289
- }
290
-
291
- /**
292
- * Render all blocks to email HTML
293
- */
294
- export function renderBlocksToEmail(blocks: Block[], context: EmailRenderContext = {}): string {
295
- return blocks.map(block => renderBlockToEmail(block, context)).join('\n');
296
- }
297
-
298
- /**
299
- * Generate complete email HTML from newsletter
300
- * Uses the same styling as the welcome email template
301
- */
302
- export function generateNewsletterEmailHtml(
303
- blocks: Block[],
304
- metadata: {
305
- subject: string;
306
- previewText?: string;
307
- },
308
- context: EmailRenderContext & {
309
- unsubscribeUrl?: string;
310
- footerText?: string;
311
- logoUrl?: string;
312
- logoAlt?: string;
313
- locale?: string;
314
- }
315
- ): string {
316
- const { baseUrl = '', unsubscribeUrl, footerText, logoUrl, logoAlt = 'Logo', locale = 'en' } = context;
317
- const contentHtml = renderBlocksToEmail(blocks, context);
318
-
319
- // Get unsubscribe text based on locale (matching welcome email)
320
- const isDutch = locale === 'nl';
321
- const unsubscribeText = isDutch ? 'Afmelden' : 'Unsubscribe';
322
-
323
- // Build header HTML if logo is provided
324
- const headerHtml = logoUrl ? `
325
- <div class="header">
326
- <img src="${logoUrl}" alt="${escapeHtml(logoAlt)}" class="logo">
327
- </div>` : '';
328
-
329
- return `<!DOCTYPE html>
330
- <html>
331
- <head>
332
- <meta charset="utf-8">
333
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
334
- <meta name="color-scheme" content="light">
335
- <style>
336
- body {
337
- background-color: #faf9f6;
338
- margin: 0;
339
- padding: 0;
340
- font-family: 'Georgia', serif;
341
- -webkit-font-smoothing: antialiased;
342
- -moz-osx-font-smoothing: grayscale;
343
- }
344
- .container {
345
- max-width: 600px;
346
- margin: 20px auto;
347
- background-color: #ffffff;
348
- border-radius: 40px;
349
- border: 1px solid #1a2e260d;
350
- overflow: hidden;
351
- }
352
- .header {
353
- padding: 40px 0 20px 0;
354
- text-align: center;
355
- }
356
- .logo {
357
- width: 180px;
358
- height: auto;
359
- }
360
- .content {
361
- padding: 0 50px 40px 50px;
362
- color: #1a2e26;
363
- line-height: 1.8;
364
- font-size: 15px;
365
- }
366
- .footer {
367
- padding: 40px 50px;
368
- text-align: center;
369
- font-family: sans-serif;
370
- font-size: 10px;
371
- color: #a1a1aa;
372
- letter-spacing: 1px;
373
- border-top: 1px solid #faf9f6;
374
- }
375
- h1 {
376
- font-weight: normal;
377
- font-style: italic;
378
- font-size: 30px;
379
- margin-bottom: 30px;
380
- color: #1a2e26;
381
- text-align: center;
382
- }
383
- .divider {
384
- height: 1px;
385
- width: 40px;
386
- background-color: #1a2e2620;
387
- margin: 30px auto;
388
- }
389
- @media only screen and (max-width: 600px) {
390
- .container {
391
- width: 100% !important;
392
- border-radius: 0 !important;
393
- }
394
- .content {
395
- padding: 0 30px 30px 30px !important;
396
- }
397
- .footer {
398
- padding: 30px !important;
399
- }
400
- }
401
- </style>
402
- </head>
403
- <body>
404
- <div class="container">
405
- ${headerHtml}
406
- <div class="content">
407
- ${contentHtml}
408
- ${unsubscribeUrl ? `
409
- <div class="divider"></div>
410
- <p style="text-align: center; font-size: 12px; color: #a1a1aa;">
411
- <a href="${unsubscribeUrl}" style="color: #a1a1aa; text-decoration: none;">
412
- ${unsubscribeText}
413
- </a>
414
- </p>
415
- ` : ''}
416
- </div>
417
- ${footerText ? `
418
- <div class="footer">
419
- ${footerText}
420
- </div>
421
- ` : ''}
422
- </div>
423
- </body>
424
- </html>`;
425
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Email Rendering Exports
3
- */
4
-
5
- export { renderBlockToEmail, renderBlocksToEmail, generateNewsletterEmailHtml } from './EmailRenderer';
6
- export type { EmailRenderContext } from './EmailRenderer';
@@ -1,57 +0,0 @@
1
- /**
2
- * API Mapper
3
- * Converts editor state to API format and vice versa
4
- */
5
-
6
- import { EditorState } from '../../state/types';
7
- import { Newsletter, NewsletterPublicationData } from '../../types/newsletter';
8
- import { Block } from '../../types/block';
9
-
10
- /**
11
- * Convert editor state to API format
12
- */
13
- export function editorStateToAPI(state: EditorState): {
14
- title: string;
15
- slug: string;
16
- blocks: Block[];
17
- metadata: {
18
- subject: string;
19
- previewText?: string;
20
- lang?: string;
21
- recipientFilter?: {
22
- type: 'all' | 'language' | 'custom';
23
- value?: string;
24
- };
25
- };
26
- publication: NewsletterPublicationData;
27
- } {
28
- // For newsletters, use subject as title if title is empty
29
- // This makes sense since email subject is the primary identifier
30
- const newsletterTitle = state.title?.trim() || state.metadata?.subject?.trim() || '';
31
-
32
- return {
33
- title: newsletterTitle,
34
- slug: state.slug,
35
- blocks: state.blocks,
36
- metadata: state.metadata,
37
- publication: {
38
- status: state.status,
39
- authorId: undefined, // Will be set by API handler
40
- updatedAt: new Date().toISOString(),
41
- },
42
- };
43
- }
44
-
45
- /**
46
- * Convert API format to editor state
47
- */
48
- export function apiToEditorState(newsletter: Newsletter): Partial<EditorState> {
49
- return {
50
- blocks: newsletter.blocks,
51
- title: newsletter.title,
52
- slug: newsletter.slug,
53
- metadata: newsletter.metadata,
54
- status: newsletter.publication.status,
55
- newsletterId: newsletter.id,
56
- };
57
- }
@@ -1,71 +0,0 @@
1
- /**
2
- * Block Helper Utilities
3
- * Functions for working with blocks, including nested structures
4
- */
5
-
6
- import { Block } from '../../types/block';
7
-
8
- /**
9
- * Get child blocks from a container block
10
- * Handles both Block[] and string[] formats
11
- */
12
- export function getChildBlocks(block: Block, allBlocks: Block[] = []): Block[] {
13
- if (!block.children || block.children.length === 0) {
14
- return [];
15
- }
16
-
17
- // If children are Block objects, return them directly
18
- if (block.children.length > 0 && typeof block.children[0] === 'object' && 'id' in block.children[0]) {
19
- return block.children as Block[];
20
- }
21
-
22
- // If children are IDs, resolve them from allBlocks (recursively search nested blocks)
23
- const childIds = block.children as string[];
24
- const resolvedBlocks: Block[] = [];
25
-
26
- function findBlockRecursive(blocks: Block[], id: string): Block | null {
27
- for (const b of blocks) {
28
- if (b.id === id) return b;
29
- if (b.children && Array.isArray(b.children) && b.children.length > 0) {
30
- if (typeof b.children[0] === 'object') {
31
- const found = findBlockRecursive(b.children as Block[], id);
32
- if (found) return found;
33
- }
34
- }
35
- }
36
- return null;
37
- }
38
-
39
- childIds.forEach(id => {
40
- const found = findBlockRecursive(allBlocks, id);
41
- if (found) resolvedBlocks.push(found);
42
- });
43
-
44
- return resolvedBlocks;
45
- }
46
-
47
- /**
48
- * Check if a block is a container (has children or is marked as container)
49
- */
50
- export function isContainerBlock(block: Block, blockRegistry: { get: (type: string) => { isContainer?: boolean } | undefined }): boolean {
51
- const definition = blockRegistry.get(block.type);
52
- return definition?.isContainer === true || (block.children !== undefined && block.children.length > 0);
53
- }
54
-
55
- /**
56
- * Find a block by ID recursively (including nested blocks)
57
- */
58
- export function findBlockById(blocks: Block[], id: string): Block | null {
59
- for (const block of blocks) {
60
- if (block.id === id) {
61
- return block;
62
- }
63
- if (block.children && Array.isArray(block.children) && block.children.length > 0) {
64
- if (typeof block.children[0] === 'object') {
65
- const found = findBlockById(block.children as Block[], id);
66
- if (found) return found;
67
- }
68
- }
69
- }
70
- return null;
71
- }