@jhits/plugin-newsletter 0.0.15 → 0.0.17
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.
- package/dist/api/email-utils.d.ts.map +1 -1
- package/dist/api/email-utils.js +45 -4
- package/dist/api/handlers/newsletters.d.ts.map +1 -1
- package/dist/api/handlers/newsletters.js +33 -16
- package/dist/api/handlers/send-newsletter.d.ts.map +1 -1
- package/dist/api/handlers/send-newsletter.js +54 -6
- package/dist/api/handlers/settings.d.ts.map +1 -1
- package/dist/api/handlers/settings.js +51 -1
- package/dist/index.d.ts +27 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -122
- package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
- package/dist/lib/blocks/BlockRenderer.js +14 -2
- package/dist/lib/email/EmailRenderer.d.ts +1 -0
- package/dist/lib/email/EmailRenderer.d.ts.map +1 -1
- package/dist/lib/email/EmailRenderer.js +31 -19
- package/dist/lib/utils/config-resolver.d.ts +33 -0
- package/dist/lib/utils/config-resolver.d.ts.map +1 -0
- package/dist/lib/utils/config-resolver.js +47 -0
- package/dist/registry/BlockRegistry.d.ts +9 -1
- package/dist/registry/BlockRegistry.d.ts.map +1 -1
- package/dist/registry/BlockRegistry.js +126 -8
- package/dist/state/EditorContext.d.ts +11 -1
- package/dist/state/EditorContext.d.ts.map +1 -1
- package/dist/state/EditorContext.js +23 -5
- package/dist/state/types.d.ts +12 -0
- package/dist/state/types.d.ts.map +1 -1
- package/dist/types/block.d.ts +9 -0
- package/dist/types/block.d.ts.map +1 -1
- package/dist/types/newsletter.d.ts +4 -0
- package/dist/types/newsletter.d.ts.map +1 -1
- package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
- package/dist/views/CanvasEditor/BlockWrapper.js +24 -3
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
- package/dist/views/CanvasEditor/CanvasEditorView.js +77 -17
- package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
- package/dist/views/CanvasEditor/EditorBody.js +1 -1
- package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorCanvas.js +158 -100
- package/dist/views/CanvasEditor/components/EditorSidebar.d.ts +3 -1
- package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorSidebar.js +3 -3
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +1 -1
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +1 -1
- package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.js +6 -40
- package/dist/views/NewsletterManager.d.ts.map +1 -1
- package/dist/views/NewsletterManager.js +87 -5
- package/dist/views/components/DomainPromptModal.d.ts +13 -0
- package/dist/views/components/DomainPromptModal.d.ts.map +1 -0
- package/dist/views/components/DomainPromptModal.js +58 -0
- package/dist/views/components/NewsletterCard.d.ts +16 -0
- package/dist/views/components/NewsletterCard.d.ts.map +1 -0
- package/dist/views/components/NewsletterCard.js +94 -0
- package/dist/views/components/NewsletterGrid.d.ts +16 -0
- package/dist/views/components/NewsletterGrid.d.ts.map +1 -0
- package/dist/views/components/NewsletterGrid.js +13 -0
- package/dist/views/components/SendNewsletterModal.d.ts.map +1 -1
- package/dist/views/components/SendNewsletterModal.js +91 -22
- package/dist/views/components/SmtpSettingsModal.d.ts.map +1 -1
- package/dist/views/components/SmtpSettingsModal.js +10 -0
- package/dist/views/components/TestEmailModal.d.ts.map +1 -1
- package/dist/views/components/TestEmailModal.js +86 -17
- package/package.json +53 -9
- package/src/api/email-utils.ts +53 -4
- package/src/api/handlers/newsletters.ts +40 -20
- package/src/api/handlers/send-newsletter.ts +65 -6
- package/src/api/handlers/settings.ts +60 -2
- package/src/index.tsx +49 -155
- package/src/lib/blocks/BlockRenderer.tsx +16 -2
- package/src/lib/email/EmailRenderer.tsx +31 -20
- package/src/lib/utils/config-resolver.ts +71 -0
- package/src/registry/BlockRegistry.tsx +255 -0
- package/src/state/EditorContext.tsx +43 -8
- package/src/state/types.ts +16 -0
- package/src/types/block.ts +10 -0
- package/src/types/newsletter.ts +5 -0
- package/src/views/CanvasEditor/BlockWrapper.tsx +27 -2
- package/src/views/CanvasEditor/CanvasEditorView.tsx +142 -61
- package/src/views/CanvasEditor/EditorBody.tsx +17 -13
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +178 -115
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +57 -2
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +6 -45
- package/src/views/NewsletterManager.tsx +164 -6
- package/src/views/components/DomainPromptModal.tsx +160 -0
- package/src/views/components/NewsletterCard.tsx +212 -0
- package/src/views/components/NewsletterGrid.tsx +48 -0
- package/src/views/components/SendNewsletterModal.tsx +270 -184
- package/src/views/components/SmtpSettingsModal.tsx +11 -0
- package/src/views/components/TestEmailModal.tsx +235 -149
- package/src/registry/BlockRegistry.ts +0 -53
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block Registry for Newsletter Plugin
|
|
3
|
+
* Dynamic registry for all block types in system
|
|
4
|
+
* Multi-Tenant Architecture: Blocks are provided by client applications
|
|
5
|
+
*
|
|
6
|
+
* The registry is a singleton that starts empty and is populated by client apps
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import {
|
|
11
|
+
BlockTypeDefinition,
|
|
12
|
+
ClientBlockDefinition,
|
|
13
|
+
BlockEditProps,
|
|
14
|
+
BlockPreviewProps
|
|
15
|
+
} from '../types/block';
|
|
16
|
+
import {
|
|
17
|
+
Type,
|
|
18
|
+
Heading as HeadingIcon,
|
|
19
|
+
List,
|
|
20
|
+
Image as ImageIcon,
|
|
21
|
+
Table as TableIcon,
|
|
22
|
+
Minus
|
|
23
|
+
} from 'lucide-react';
|
|
24
|
+
|
|
25
|
+
// --- Default Block Components ---
|
|
26
|
+
|
|
27
|
+
// Paragraph
|
|
28
|
+
const DefaultParagraphEdit: React.FC<BlockEditProps> = ({ block, onUpdate }) => (
|
|
29
|
+
<textarea
|
|
30
|
+
value={(block.data.text as string) || ''}
|
|
31
|
+
onChange={(e) => onUpdate({ text: e.target.value })}
|
|
32
|
+
onInput={(e) => {
|
|
33
|
+
const target = e.target as HTMLTextAreaElement;
|
|
34
|
+
target.style.height = 'auto';
|
|
35
|
+
target.style.height = `${target.scrollHeight}px`;
|
|
36
|
+
}}
|
|
37
|
+
placeholder="Type your text..."
|
|
38
|
+
className="w-full bg-transparent border-none outline-none focus:ring-0 p-0 resize-none min-h-[1.5em] font-serif text-[18px] leading-[1.625] text-[#1a2e26] overflow-hidden"
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
const DefaultParagraphPreview: React.FC<BlockPreviewProps> = ({ block }) => (
|
|
42
|
+
<p
|
|
43
|
+
className="font-serif text-[18px] leading-[1.625] text-[#1a2e26]"
|
|
44
|
+
dangerouslySetInnerHTML={{ __html: (block.data.html as string) || (block.data.text as string) || '' }}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Heading
|
|
49
|
+
const DefaultHeadingEdit: React.FC<BlockEditProps> = ({ block, onUpdate }) => {
|
|
50
|
+
const level = (block.data.level as number) || 2;
|
|
51
|
+
const fontSize = level === 1 ? '48px' : level === 2 ? '36px' : level === 3 ? '30px' : '20px';
|
|
52
|
+
return (
|
|
53
|
+
<input
|
|
54
|
+
type="text"
|
|
55
|
+
value={(block.data.text as string) || ''}
|
|
56
|
+
onChange={(e) => onUpdate({ text: e.target.value })}
|
|
57
|
+
placeholder="Heading..."
|
|
58
|
+
style={{ fontSize, lineHeight: '1.2' }}
|
|
59
|
+
className="w-full bg-transparent border-none outline-none focus:ring-0 p-0 font-serif font-bold text-[#1a2e26]"
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
const DefaultHeadingPreview: React.FC<BlockPreviewProps> = ({ block }) => {
|
|
64
|
+
const level = (block.data.level as number) || 2;
|
|
65
|
+
const fontSize = level === 1 ? '48px' : level === 2 ? '36px' : level === 3 ? '30px' : '20px';
|
|
66
|
+
const Tag = `h${level}` as keyof React.JSX.IntrinsicElements;
|
|
67
|
+
return (
|
|
68
|
+
<Tag
|
|
69
|
+
style={{ fontSize, lineHeight: '1.2' }}
|
|
70
|
+
className="font-serif font-bold text-[#1a2e26]"
|
|
71
|
+
dangerouslySetInnerHTML={{ __html: (block.data.text as string) || '' }}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// List
|
|
77
|
+
const DefaultListEdit: React.FC<BlockEditProps> = ({ block, onUpdate }) => {
|
|
78
|
+
const items = Array.isArray(block.data.items) ? block.data.items : [''];
|
|
79
|
+
return (
|
|
80
|
+
<div className="space-y-2">
|
|
81
|
+
{items.map((item, i) => (
|
|
82
|
+
<div key={i} className="flex gap-2">
|
|
83
|
+
<span className="text-neutral-400">•</span>
|
|
84
|
+
<input
|
|
85
|
+
type="text"
|
|
86
|
+
value={typeof item === 'string' ? item : item.text || ''}
|
|
87
|
+
onChange={(e) => {
|
|
88
|
+
const newItems = [...items];
|
|
89
|
+
newItems[i] = e.target.value;
|
|
90
|
+
onUpdate({ items: newItems });
|
|
91
|
+
}}
|
|
92
|
+
className="flex-1 bg-transparent border-none outline-none focus:ring-0 p-0 text-[18px] leading-[1.625] font-serif"
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
))}
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={() => onUpdate({ items: [...items, ''] })}
|
|
99
|
+
className="text-xs text-primary font-bold hover:underline"
|
|
100
|
+
>
|
|
101
|
+
+ Add Item
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
const DefaultListPreview: React.FC<BlockPreviewProps> = ({ block }) => {
|
|
107
|
+
const items = Array.isArray(block.data.items) ? block.data.items : [];
|
|
108
|
+
const Tag = block.data.type === 'ol' ? 'ol' : 'ul';
|
|
109
|
+
return (
|
|
110
|
+
<Tag className="list-disc pl-5 space-y-1 font-serif text-[18px] leading-[1.625] text-[#1a2e26]">
|
|
111
|
+
{items.map((item, i) => (
|
|
112
|
+
<li key={i}>{typeof item === 'string' ? item : item.text || ''}</li>
|
|
113
|
+
))}
|
|
114
|
+
</Tag>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Local interface to avoid import issues
|
|
119
|
+
interface IBlockRegistry {
|
|
120
|
+
register(definition: BlockTypeDefinition): void;
|
|
121
|
+
registerClientBlocks(definitions: ClientBlockDefinition[]): void;
|
|
122
|
+
get(type: string): BlockTypeDefinition | undefined;
|
|
123
|
+
getAll(): BlockTypeDefinition[];
|
|
124
|
+
has(type: string): boolean;
|
|
125
|
+
clear(): void;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Block Registry Implementation
|
|
130
|
+
* Singleton that manages all block types in the system
|
|
131
|
+
*/
|
|
132
|
+
class BlockRegistryImpl implements IBlockRegistry {
|
|
133
|
+
private blocks: Map<string, BlockTypeDefinition> = new Map();
|
|
134
|
+
|
|
135
|
+
constructor() {
|
|
136
|
+
// Register default blocks on instantiation (server & client)
|
|
137
|
+
this.registerDefaultBlocks();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private registerDefaultBlocks(): void {
|
|
141
|
+
const defaults: BlockTypeDefinition[] = [
|
|
142
|
+
{
|
|
143
|
+
type: 'paragraph',
|
|
144
|
+
name: 'Paragraph',
|
|
145
|
+
description: 'Simple text paragraph',
|
|
146
|
+
icon: Type,
|
|
147
|
+
category: 'text',
|
|
148
|
+
defaultData: { text: '' },
|
|
149
|
+
components: { Edit: DefaultParagraphEdit, Preview: DefaultParagraphPreview, Icon: Type }
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'heading',
|
|
153
|
+
name: 'Heading',
|
|
154
|
+
description: 'Section heading',
|
|
155
|
+
icon: HeadingIcon,
|
|
156
|
+
category: 'text',
|
|
157
|
+
defaultData: { text: '', level: 2 },
|
|
158
|
+
components: { Edit: DefaultHeadingEdit, Preview: DefaultHeadingPreview, Icon: HeadingIcon }
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
type: 'list',
|
|
162
|
+
name: 'List',
|
|
163
|
+
description: 'Bullet or numbered list',
|
|
164
|
+
icon: List,
|
|
165
|
+
category: 'text',
|
|
166
|
+
defaultData: { items: [''], type: 'ul' },
|
|
167
|
+
components: { Edit: DefaultListEdit, Preview: DefaultListPreview, Icon: List }
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
type: 'divider',
|
|
171
|
+
name: 'Divider',
|
|
172
|
+
description: 'Horizontal separator',
|
|
173
|
+
icon: Minus,
|
|
174
|
+
category: 'layout',
|
|
175
|
+
defaultData: {},
|
|
176
|
+
components: {
|
|
177
|
+
Edit: () => <hr className="my-4 border-t border-neutral-200" />,
|
|
178
|
+
Preview: () => <hr className="my-4 border-t border-neutral-200" />,
|
|
179
|
+
Icon: Minus
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
for (const def of defaults) {
|
|
185
|
+
// Use register to ensure it's also added to window
|
|
186
|
+
this.register(def);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
register(definition: BlockTypeDefinition): void {
|
|
191
|
+
this.blocks.set(definition.type, definition);
|
|
192
|
+
|
|
193
|
+
// Also attach to window for cross-context access
|
|
194
|
+
if (typeof window !== 'undefined') {
|
|
195
|
+
if (!(window as any).__JHITS_NEWSLETTER_REGISTRY__) {
|
|
196
|
+
(window as any).__JHITS_NEWSLETTER_REGISTRY__ = new Map();
|
|
197
|
+
}
|
|
198
|
+
(window as any).__JHITS_NEWSLETTER_REGISTRY__.set(definition.type, definition);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Register multiple client blocks at once
|
|
204
|
+
* This is the primary method for client applications to register their blocks
|
|
205
|
+
*/
|
|
206
|
+
registerClientBlocks(definitions: ClientBlockDefinition[]): void {
|
|
207
|
+
for (const def of definitions) {
|
|
208
|
+
if (!def.type || !def.name || !def.components) continue;
|
|
209
|
+
if (!def.components.Edit || !def.components.Preview) continue;
|
|
210
|
+
|
|
211
|
+
const blockDefinition: BlockTypeDefinition = {
|
|
212
|
+
type: def.type,
|
|
213
|
+
name: def.name,
|
|
214
|
+
description: def.description,
|
|
215
|
+
icon: def.icon || def.components.Icon,
|
|
216
|
+
defaultData: def.defaultData,
|
|
217
|
+
validate: def.validate,
|
|
218
|
+
isContainer: def.isContainer,
|
|
219
|
+
allowedChildren: def.allowedChildren,
|
|
220
|
+
category: def.category || 'custom',
|
|
221
|
+
components: def.components,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
this.register(blockDefinition);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
get(type: string): BlockTypeDefinition | undefined {
|
|
229
|
+
const block = this.blocks.get(type);
|
|
230
|
+
if (block) return block;
|
|
231
|
+
|
|
232
|
+
if (typeof window !== 'undefined' && (window as any).__JHITS_NEWSLETTER_REGISTRY__) {
|
|
233
|
+
return (window as any).__JHITS_NEWSLETTER_REGISTRY__.get(type);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
getAll(): BlockTypeDefinition[] {
|
|
240
|
+
return Array.from(this.blocks.values());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
has(type: string): boolean {
|
|
244
|
+
return this.blocks.has(type);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
clear(): void {
|
|
248
|
+
this.blocks.clear();
|
|
249
|
+
this.registerDefaultBlocks(); // Re-add defaults after clear
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Export singleton instance
|
|
254
|
+
export const BlockRegistry = new BlockRegistryImpl();
|
|
255
|
+
export const blockRegistry = BlockRegistry;
|
|
@@ -40,8 +40,18 @@ export interface EditorProviderProps {
|
|
|
40
40
|
/** Background color for dark mode (optional) */
|
|
41
41
|
dark?: string;
|
|
42
42
|
};
|
|
43
|
+
/** Localized strings for modular blocks in the editor */
|
|
44
|
+
translations?: Record<string, any>;
|
|
43
45
|
/** If true, this editor is for the welcome email */
|
|
44
46
|
isWelcomeEmail?: boolean;
|
|
47
|
+
/** Email configuration (logo, footer, etc.) */
|
|
48
|
+
emailConfig?: {
|
|
49
|
+
logoUrl?: string;
|
|
50
|
+
logoAlt?: string;
|
|
51
|
+
footerText?: string;
|
|
52
|
+
};
|
|
53
|
+
/** Global translations for unsubscribe text */
|
|
54
|
+
unsubscribeTranslations?: Record<string, string>;
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
/**
|
|
@@ -56,23 +66,44 @@ export function EditorProvider({
|
|
|
56
66
|
customBlocks = [],
|
|
57
67
|
darkMode = true,
|
|
58
68
|
backgroundColors,
|
|
59
|
-
|
|
69
|
+
translations,
|
|
70
|
+
isWelcomeEmail,
|
|
71
|
+
emailConfig,
|
|
72
|
+
unsubscribeTranslations
|
|
60
73
|
}: EditorProviderProps) {
|
|
61
|
-
|
|
74
|
+
const [state, dispatch] = useReducer(
|
|
75
|
+
editorReducer,
|
|
76
|
+
{ ...initialEditorState, ...initialState }
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Track registered blocks in state to trigger re-renders
|
|
80
|
+
const [registeredBlockTypes, setRegisteredBlockBlocks] = useState<string[]>(() =>
|
|
81
|
+
BlockRegistry.getAll().map(b => b.type)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Register client blocks on mount or when they change
|
|
62
85
|
useEffect(() => {
|
|
63
86
|
if (customBlocks && customBlocks.length > 0) {
|
|
64
87
|
try {
|
|
65
|
-
|
|
88
|
+
BlockRegistry.registerClientBlocks(customBlocks);
|
|
89
|
+
// Update state to trigger re-render
|
|
90
|
+
setRegisteredBlockBlocks(BlockRegistry.getAll().map(b => b.type));
|
|
66
91
|
} catch (error) {
|
|
67
92
|
console.error('[NewsletterEditorContext] Failed to register custom blocks:', error);
|
|
68
93
|
}
|
|
69
94
|
}
|
|
70
95
|
}, [customBlocks]);
|
|
71
96
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
// Periodically check for new blocks in global registry (fallback for late loads)
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const interval = setInterval(() => {
|
|
100
|
+
const currentTypes = BlockRegistry.getAll().map(b => b.type);
|
|
101
|
+
if (currentTypes.length !== registeredBlockTypes.length) {
|
|
102
|
+
setRegisteredBlockBlocks(currentTypes);
|
|
103
|
+
}
|
|
104
|
+
}, 1000);
|
|
105
|
+
return () => clearInterval(interval);
|
|
106
|
+
}, [registeredBlockTypes.length]);
|
|
76
107
|
|
|
77
108
|
// Use a ref to always have access to the latest state in callbacks
|
|
78
109
|
const stateRef = useRef(state);
|
|
@@ -244,6 +275,10 @@ export function EditorProvider({
|
|
|
244
275
|
dispatch,
|
|
245
276
|
darkMode,
|
|
246
277
|
backgroundColors,
|
|
278
|
+
translations,
|
|
279
|
+
emailConfig,
|
|
280
|
+
unsubscribeTranslations,
|
|
281
|
+
registeredBlockTypes,
|
|
247
282
|
helpers: {
|
|
248
283
|
addBlock,
|
|
249
284
|
updateBlock,
|
|
@@ -259,7 +294,7 @@ export function EditorProvider({
|
|
|
259
294
|
canUndo: historyIndex > 0 && history.length > 0,
|
|
260
295
|
canRedo: historyIndex < history.length - 1,
|
|
261
296
|
}),
|
|
262
|
-
[state, dispatch, darkMode, backgroundColors, addBlock, updateBlock, deleteBlock, duplicateBlock, moveBlock, loadNewsletter, resetEditor, save, undo, redo, historyIndex, history.length]
|
|
297
|
+
[state, dispatch, darkMode, backgroundColors, translations, emailConfig, unsubscribeTranslations, registeredBlockTypes, addBlock, updateBlock, deleteBlock, duplicateBlock, moveBlock, loadNewsletter, resetEditor, save, undo, redo, historyIndex, history.length]
|
|
263
298
|
);
|
|
264
299
|
|
|
265
300
|
return <EditorContext.Provider value={value}>{children}</EditorContext.Provider>;
|
package/src/state/types.ts
CHANGED
|
@@ -90,6 +90,22 @@ export interface EditorContextValue {
|
|
|
90
90
|
dark?: string;
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
/** Localized strings for modular blocks in the editor */
|
|
94
|
+
translations?: Record<string, any>;
|
|
95
|
+
|
|
96
|
+
/** Email configuration (logo, footer, etc.) */
|
|
97
|
+
emailConfig?: {
|
|
98
|
+
logoUrl?: string;
|
|
99
|
+
logoAlt?: string;
|
|
100
|
+
footerText?: string;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Global translations for unsubscribe text */
|
|
104
|
+
unsubscribeTranslations?: Record<string, string>;
|
|
105
|
+
|
|
106
|
+
/** List of registered block types (used to trigger re-renders) */
|
|
107
|
+
registeredBlockTypes: string[];
|
|
108
|
+
|
|
93
109
|
/** Helper functions for common operations */
|
|
94
110
|
helpers: {
|
|
95
111
|
/** Add a new block (supports nested containers via containerId) */
|
package/src/types/block.ts
CHANGED
|
@@ -70,6 +70,16 @@ export interface BlockEditProps {
|
|
|
70
70
|
|
|
71
71
|
/** Focus mode state */
|
|
72
72
|
focusMode?: boolean;
|
|
73
|
+
|
|
74
|
+
/** Additional rendering context */
|
|
75
|
+
context?: {
|
|
76
|
+
/** Site ID */
|
|
77
|
+
siteId?: string;
|
|
78
|
+
/** Locale */
|
|
79
|
+
locale?: string;
|
|
80
|
+
/** Custom render props (e.g. translations) */
|
|
81
|
+
[key: string]: unknown;
|
|
82
|
+
};
|
|
73
83
|
|
|
74
84
|
/** Child blocks (for container blocks like Section, Columns) */
|
|
75
85
|
childBlocks?: Block[];
|
package/src/types/newsletter.ts
CHANGED
|
@@ -79,6 +79,9 @@ export interface NewsletterMetadata {
|
|
|
79
79
|
/** Preview text */
|
|
80
80
|
previewText?: string;
|
|
81
81
|
|
|
82
|
+
/** Unsubscribe text (localized) */
|
|
83
|
+
unsubscribeText?: string;
|
|
84
|
+
|
|
82
85
|
/** Language code */
|
|
83
86
|
lang?: string;
|
|
84
87
|
|
|
@@ -160,6 +163,8 @@ export interface NewsletterListItem {
|
|
|
160
163
|
recipientCount?: number;
|
|
161
164
|
hidden?: boolean;
|
|
162
165
|
sendHistory?: SendHistoryEntry[];
|
|
166
|
+
availableLanguages?: string[];
|
|
167
|
+
languages?: NewsletterLanguages;
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
/**
|
|
@@ -42,7 +42,10 @@ export function BlockWrapper({
|
|
|
42
42
|
onAddBlockBelow,
|
|
43
43
|
}: BlockWrapperProps) {
|
|
44
44
|
const [isHovered, setIsHovered] = useState(false);
|
|
45
|
-
const { state } = useEditor();
|
|
45
|
+
const { state, translations, registeredBlockTypes } = useEditor();
|
|
46
|
+
|
|
47
|
+
// We use the block definition from registry
|
|
48
|
+
// The component will re-render when registeredBlockTypes changes
|
|
46
49
|
const blockDefinition = blockRegistry.get(block.type);
|
|
47
50
|
|
|
48
51
|
// Check if this is a container block
|
|
@@ -65,6 +68,20 @@ export function BlockWrapper({
|
|
|
65
68
|
|
|
66
69
|
const EditComponent = blockDefinition.components.Edit;
|
|
67
70
|
|
|
71
|
+
// Helper to check if block is empty (for easier deletion)
|
|
72
|
+
const isBlockEmpty = () => {
|
|
73
|
+
if (block.type === 'paragraph' || block.type === 'heading') {
|
|
74
|
+
const text = (block.data.text as string) || '';
|
|
75
|
+
const html = (block.data.html as string) || '';
|
|
76
|
+
return text.trim() === '' && html.trim() === '';
|
|
77
|
+
}
|
|
78
|
+
if (block.type === 'list') {
|
|
79
|
+
const items = (block.data.items as any[]) || [];
|
|
80
|
+
return items.length === 0 || (items.length === 1 && (typeof items[0] === 'string' ? items[0].trim() === '' : items[0].text?.trim() === ''));
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
};
|
|
84
|
+
|
|
68
85
|
// Store block ID when hovering for paste context
|
|
69
86
|
useEffect(() => {
|
|
70
87
|
if (isHovered) {
|
|
@@ -103,6 +120,10 @@ export function BlockWrapper({
|
|
|
103
120
|
onDelete={onDelete}
|
|
104
121
|
isSelected={state.selectedBlockId === block.id}
|
|
105
122
|
childBlocks={childBlocks}
|
|
123
|
+
context={{
|
|
124
|
+
locale: state.metadata.lang || 'en',
|
|
125
|
+
translations
|
|
126
|
+
}}
|
|
106
127
|
/>
|
|
107
128
|
</div>
|
|
108
129
|
</SlashCommandDetector>
|
|
@@ -114,6 +135,10 @@ export function BlockWrapper({
|
|
|
114
135
|
onDelete={onDelete}
|
|
115
136
|
isSelected={state.selectedBlockId === block.id}
|
|
116
137
|
childBlocks={childBlocks}
|
|
138
|
+
context={{
|
|
139
|
+
locale: state.metadata.lang || 'en',
|
|
140
|
+
translations
|
|
141
|
+
}}
|
|
117
142
|
/>
|
|
118
143
|
</div>
|
|
119
144
|
)}
|
|
@@ -128,7 +153,7 @@ export function BlockWrapper({
|
|
|
128
153
|
<button
|
|
129
154
|
onClick={(e) => {
|
|
130
155
|
e.stopPropagation();
|
|
131
|
-
if (confirm('Delete this block?')) {
|
|
156
|
+
if (isBlockEmpty() || confirm('Delete this block?')) {
|
|
132
157
|
onDelete();
|
|
133
158
|
}
|
|
134
159
|
}}
|