@jhits/plugin-newsletter 0.0.7 → 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.
- package/package.json +2 -3
- package/src/api/handler.ts +0 -693
- package/src/api/router.ts +0 -111
- package/src/index.server.ts +0 -12
- package/src/index.tsx +0 -313
- package/src/index.tsx.patch +0 -98
- package/src/init.tsx +0 -72
- package/src/lib/blocks/BlockRenderer.tsx +0 -125
- package/src/lib/email/EmailRenderer.tsx +0 -425
- package/src/lib/email/index.ts +0 -6
- package/src/lib/mappers/apiMapper.ts +0 -57
- package/src/lib/utils/blockHelpers.ts +0 -71
- package/src/lib/utils/slugify.ts +0 -43
- package/src/registry/BlockRegistry.ts +0 -53
- package/src/registry/index.ts +0 -5
- package/src/state/EditorContext.tsx +0 -279
- package/src/state/index.ts +0 -10
- package/src/state/reducer.ts +0 -561
- package/src/state/types.ts +0 -154
- package/src/types/block.ts +0 -275
- package/src/types/newsletter.ts +0 -151
- package/src/types/registry.ts +0 -14
- package/src/views/CanvasEditor/BlockWrapper.tsx +0 -143
- package/src/views/CanvasEditor/CanvasEditorView.tsx +0 -249
- package/src/views/CanvasEditor/EditorBody.tsx +0 -95
- package/src/views/CanvasEditor/EditorHeader.tsx +0 -139
- package/src/views/CanvasEditor/components/CustomBlockItem.tsx +0 -83
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +0 -674
- package/src/views/CanvasEditor/components/EditorLibrary.tsx +0 -120
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +0 -156
- package/src/views/CanvasEditor/components/ErrorBanner.tsx +0 -31
- package/src/views/CanvasEditor/components/LibraryItem.tsx +0 -71
- package/src/views/CanvasEditor/components/SlashCommandDetector.tsx +0 -196
- package/src/views/CanvasEditor/components/SlashCommandMenu.tsx +0 -131
- package/src/views/CanvasEditor/components/index.ts +0 -16
- package/src/views/CanvasEditor/hooks/index.ts +0 -7
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +0 -136
- package/src/views/CanvasEditor/hooks/useNewsletterLoader.ts +0 -34
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +0 -54
- package/src/views/CanvasEditor/hooks/useSlashCommand.ts +0 -106
- package/src/views/CanvasEditor/index.ts +0 -12
- package/src/views/NewsletterEditor.tsx +0 -38
- package/src/views/NewsletterManager.tsx +0 -240
- package/src/views/SettingsView.tsx +0 -216
- package/src/views/SubscribersView.tsx +0 -269
package/src/lib/utils/slugify.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Slug Utilities
|
|
3
|
-
* Functions for generating and validating URL slugs
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Convert a string to a URL-friendly slug
|
|
8
|
-
*/
|
|
9
|
-
export function slugify(text: string): string {
|
|
10
|
-
return text
|
|
11
|
-
.toString()
|
|
12
|
-
.toLowerCase()
|
|
13
|
-
.trim()
|
|
14
|
-
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
15
|
-
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
|
|
16
|
-
.replace(/\-\-+/g, '-') // Replace multiple hyphens with single hyphen
|
|
17
|
-
.replace(/^-+/, '') // Trim hyphens from start
|
|
18
|
-
.replace(/-+$/, ''); // Trim hyphens from end
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Generate a slug from a title
|
|
23
|
-
* Automatically handles edge cases and ensures uniqueness
|
|
24
|
-
*/
|
|
25
|
-
export function generateSlugFromTitle(title: string, existingSlugs: string[] = []): string {
|
|
26
|
-
let baseSlug = slugify(title);
|
|
27
|
-
|
|
28
|
-
// If slug is empty after processing, use a fallback
|
|
29
|
-
if (!baseSlug) {
|
|
30
|
-
baseSlug = 'untitled-newsletter';
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Check for collisions and append number if needed
|
|
34
|
-
let finalSlug = baseSlug;
|
|
35
|
-
let counter = 1;
|
|
36
|
-
|
|
37
|
-
while (existingSlugs.includes(finalSlug)) {
|
|
38
|
-
finalSlug = `${baseSlug}-${counter}`;
|
|
39
|
-
counter++;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return finalSlug;
|
|
43
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
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 {
|
|
10
|
-
BlockTypeDefinition,
|
|
11
|
-
ClientBlockDefinition
|
|
12
|
-
} from '../types/block';
|
|
13
|
-
|
|
14
|
-
// Local interface to avoid import issues
|
|
15
|
-
interface IBlockRegistry {
|
|
16
|
-
register(definition: BlockTypeDefinition): void;
|
|
17
|
-
get(type: string): BlockTypeDefinition | undefined;
|
|
18
|
-
getAll(): BlockTypeDefinition[];
|
|
19
|
-
has(type: string): boolean;
|
|
20
|
-
clear(): void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Block Registry Implementation
|
|
25
|
-
* Singleton that manages all block types in the system
|
|
26
|
-
*/
|
|
27
|
-
class BlockRegistryImpl implements IBlockRegistry {
|
|
28
|
-
private blocks: Map<string, BlockTypeDefinition> = new Map();
|
|
29
|
-
|
|
30
|
-
register(definition: BlockTypeDefinition): void {
|
|
31
|
-
this.blocks.set(definition.type, definition);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
get(type: string): BlockTypeDefinition | undefined {
|
|
35
|
-
return this.blocks.get(type);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
getAll(): BlockTypeDefinition[] {
|
|
39
|
-
return Array.from(this.blocks.values());
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
has(type: string): boolean {
|
|
43
|
-
return this.blocks.has(type);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
clear(): void {
|
|
47
|
-
this.blocks.clear();
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Export singleton instance
|
|
52
|
-
export const BlockRegistry = new BlockRegistryImpl();
|
|
53
|
-
export const blockRegistry = BlockRegistry;
|
package/src/registry/index.ts
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Newsletter Editor Context
|
|
3
|
-
* React Context for managing newsletter editor state
|
|
4
|
-
* Multi-Tenant: Accepts custom blocks from client applications
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
'use client';
|
|
8
|
-
|
|
9
|
-
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect, useRef, useState } from 'react';
|
|
10
|
-
import { editorReducer } from './reducer';
|
|
11
|
-
import { EditorState, EditorAction, initialEditorState, EditorContextValue } from './types';
|
|
12
|
-
import { Block } from '../types/block';
|
|
13
|
-
import { Newsletter } from '../types/newsletter';
|
|
14
|
-
import { ClientBlockDefinition } from '../types/block';
|
|
15
|
-
import { BlockRegistry } from '../registry/BlockRegistry';
|
|
16
|
-
|
|
17
|
-
// Create the context
|
|
18
|
-
const EditorContext = createContext<EditorContextValue | null>(null);
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Editor Provider Props
|
|
22
|
-
*/
|
|
23
|
-
export interface EditorProviderProps {
|
|
24
|
-
children: React.ReactNode;
|
|
25
|
-
/** Initial state (optional) */
|
|
26
|
-
initialState?: Partial<EditorState>;
|
|
27
|
-
/** Callback when save is triggered */
|
|
28
|
-
onSave?: (state: EditorState) => Promise<void>;
|
|
29
|
-
/**
|
|
30
|
-
* Custom blocks from client application
|
|
31
|
-
* These blocks will be registered in the BlockRegistry on mount
|
|
32
|
-
*/
|
|
33
|
-
customBlocks?: ClientBlockDefinition[];
|
|
34
|
-
/** Enable dark mode for content area and wrappers (default: true) */
|
|
35
|
-
darkMode?: boolean;
|
|
36
|
-
/** Background colors for the editor */
|
|
37
|
-
backgroundColors?: {
|
|
38
|
-
/** Background color for light mode (REQUIRED) */
|
|
39
|
-
light: string;
|
|
40
|
-
/** Background color for dark mode (optional) */
|
|
41
|
-
dark?: string;
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Editor Provider
|
|
47
|
-
* Provides editor state and actions to child components
|
|
48
|
-
* Automatically registers client-provided blocks on mount
|
|
49
|
-
*/
|
|
50
|
-
export function EditorProvider({
|
|
51
|
-
children,
|
|
52
|
-
initialState,
|
|
53
|
-
onSave,
|
|
54
|
-
customBlocks = [],
|
|
55
|
-
darkMode = true,
|
|
56
|
-
backgroundColors
|
|
57
|
-
}: EditorProviderProps) {
|
|
58
|
-
// Register client blocks on mount
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
if (customBlocks && customBlocks.length > 0) {
|
|
61
|
-
try {
|
|
62
|
-
customBlocks.forEach(block => BlockRegistry.register(block));
|
|
63
|
-
} catch (error) {
|
|
64
|
-
console.error('[NewsletterEditorContext] Failed to register custom blocks:', error);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}, [customBlocks]);
|
|
68
|
-
|
|
69
|
-
const [state, dispatch] = useReducer(
|
|
70
|
-
editorReducer,
|
|
71
|
-
{ ...initialEditorState, ...initialState }
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
// Use a ref to always have access to the latest state in callbacks
|
|
75
|
-
const stateRef = useRef(state);
|
|
76
|
-
stateRef.current = state;
|
|
77
|
-
|
|
78
|
-
// History state for undo/redo
|
|
79
|
-
const [history, setHistory] = useState<EditorState[]>([]);
|
|
80
|
-
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
81
|
-
const isRestoringRef = useRef(false);
|
|
82
|
-
const MAX_HISTORY = 50; // Limit history to prevent memory issues
|
|
83
|
-
|
|
84
|
-
// Save current state to history after state changes (but not during undo/redo)
|
|
85
|
-
// Debounce history updates to avoid excessive re-renders
|
|
86
|
-
const historyTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
87
|
-
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
if (isRestoringRef.current) {
|
|
90
|
-
isRestoringRef.current = false;
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Clear existing timeout
|
|
95
|
-
if (historyTimeoutRef.current) {
|
|
96
|
-
clearTimeout(historyTimeoutRef.current);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Debounce history updates to reduce re-renders
|
|
100
|
-
historyTimeoutRef.current = setTimeout(() => {
|
|
101
|
-
// Save current state to history
|
|
102
|
-
setHistory(prev => {
|
|
103
|
-
const newHistory = [...prev];
|
|
104
|
-
// Remove any future history if we're not at the end
|
|
105
|
-
if (historyIndex < newHistory.length - 1) {
|
|
106
|
-
newHistory.splice(historyIndex + 1);
|
|
107
|
-
}
|
|
108
|
-
// Add current state
|
|
109
|
-
newHistory.push({ ...state });
|
|
110
|
-
// Limit history size
|
|
111
|
-
if (newHistory.length > MAX_HISTORY) {
|
|
112
|
-
newHistory.shift();
|
|
113
|
-
return newHistory;
|
|
114
|
-
}
|
|
115
|
-
return newHistory;
|
|
116
|
-
});
|
|
117
|
-
setHistoryIndex(prev => {
|
|
118
|
-
const newIndex = prev + 1;
|
|
119
|
-
return newIndex >= MAX_HISTORY ? MAX_HISTORY - 1 : newIndex;
|
|
120
|
-
});
|
|
121
|
-
}, 300); // Debounce by 300ms
|
|
122
|
-
|
|
123
|
-
return () => {
|
|
124
|
-
if (historyTimeoutRef.current) {
|
|
125
|
-
clearTimeout(historyTimeoutRef.current);
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
}, [state.blocks, state.title, state.slug, state.metadata, state.status, historyIndex]);
|
|
129
|
-
|
|
130
|
-
// Helper: Add a new block (supports nested containers)
|
|
131
|
-
const addBlock = useCallback((type: string, index?: number, containerId?: string) => {
|
|
132
|
-
const blockDefinition = BlockRegistry.get(type);
|
|
133
|
-
if (!blockDefinition) {
|
|
134
|
-
console.warn(`Block type "${type}" not found in registry. Available types:`,
|
|
135
|
-
BlockRegistry.getAll().map((b: any) => b.type).join(', '));
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const newBlock: Block = {
|
|
140
|
-
id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
141
|
-
type,
|
|
142
|
-
data: { ...blockDefinition.defaultData },
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
dispatch({ type: 'ADD_BLOCK', payload: { block: newBlock, index, containerId } });
|
|
146
|
-
}, []);
|
|
147
|
-
|
|
148
|
-
// Helper: Update a block
|
|
149
|
-
const updateBlock = useCallback((id: string, data: Partial<Block['data']>) => {
|
|
150
|
-
dispatch({ type: 'UPDATE_BLOCK', payload: { id, data } });
|
|
151
|
-
}, []);
|
|
152
|
-
|
|
153
|
-
// Helper: Delete a block
|
|
154
|
-
const deleteBlock = useCallback((id: string) => {
|
|
155
|
-
dispatch({ type: 'DELETE_BLOCK', payload: { id } });
|
|
156
|
-
}, []);
|
|
157
|
-
|
|
158
|
-
// Helper: Duplicate a block
|
|
159
|
-
const duplicateBlock = useCallback((id: string) => {
|
|
160
|
-
dispatch({ type: 'DUPLICATE_BLOCK', payload: { id } });
|
|
161
|
-
}, []);
|
|
162
|
-
|
|
163
|
-
// Helper: Move a block (supports nested containers)
|
|
164
|
-
const moveBlock = useCallback((id: string, newIndex: number, containerId?: string) => {
|
|
165
|
-
dispatch({ type: 'MOVE_BLOCK', payload: { id, newIndex, containerId } });
|
|
166
|
-
}, []);
|
|
167
|
-
|
|
168
|
-
// Helper: Load a newsletter
|
|
169
|
-
const loadNewsletter = useCallback((newsletter: Newsletter) => {
|
|
170
|
-
dispatch({ type: 'LOAD_NEWSLETTER', payload: newsletter });
|
|
171
|
-
}, []);
|
|
172
|
-
|
|
173
|
-
// Helper: Reset editor
|
|
174
|
-
const resetEditor = useCallback(() => {
|
|
175
|
-
dispatch({ type: 'RESET_EDITOR' });
|
|
176
|
-
}, []);
|
|
177
|
-
|
|
178
|
-
// Helper: Save
|
|
179
|
-
// Uses stateRef to always get the latest state, avoiding stale closure issues
|
|
180
|
-
const save = useCallback(async () => {
|
|
181
|
-
if (onSave) {
|
|
182
|
-
// Use stateRef.current to get the absolute latest state
|
|
183
|
-
// This ensures we don't have stale closure issues with React state updates
|
|
184
|
-
await onSave(stateRef.current);
|
|
185
|
-
dispatch({ type: 'MARK_CLEAN' });
|
|
186
|
-
}
|
|
187
|
-
}, [onSave]);
|
|
188
|
-
|
|
189
|
-
// Helper: Undo
|
|
190
|
-
const undo = useCallback(() => {
|
|
191
|
-
if (historyIndex > 0 && history.length > 0) {
|
|
192
|
-
const previousState = history[historyIndex - 1];
|
|
193
|
-
if (previousState) {
|
|
194
|
-
isRestoringRef.current = true;
|
|
195
|
-
setHistoryIndex(prev => prev - 1);
|
|
196
|
-
dispatch({
|
|
197
|
-
type: 'LOAD_NEWSLETTER', payload: {
|
|
198
|
-
id: previousState.newsletterId || '',
|
|
199
|
-
title: previousState.title,
|
|
200
|
-
slug: previousState.slug,
|
|
201
|
-
blocks: previousState.blocks,
|
|
202
|
-
publication: {
|
|
203
|
-
status: previousState.status,
|
|
204
|
-
authorId: undefined,
|
|
205
|
-
},
|
|
206
|
-
metadata: previousState.metadata,
|
|
207
|
-
createdAt: new Date().toISOString(),
|
|
208
|
-
updatedAt: new Date().toISOString(),
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}, [history, historyIndex, dispatch]);
|
|
214
|
-
|
|
215
|
-
// Helper: Redo
|
|
216
|
-
const redo = useCallback(() => {
|
|
217
|
-
if (historyIndex < history.length - 1) {
|
|
218
|
-
const nextState = history[historyIndex + 1];
|
|
219
|
-
if (nextState) {
|
|
220
|
-
isRestoringRef.current = true;
|
|
221
|
-
setHistoryIndex(prev => prev + 1);
|
|
222
|
-
dispatch({
|
|
223
|
-
type: 'LOAD_NEWSLETTER', payload: {
|
|
224
|
-
id: nextState.newsletterId || '',
|
|
225
|
-
title: nextState.title,
|
|
226
|
-
slug: nextState.slug,
|
|
227
|
-
blocks: nextState.blocks,
|
|
228
|
-
publication: {
|
|
229
|
-
status: nextState.status,
|
|
230
|
-
authorId: undefined,
|
|
231
|
-
},
|
|
232
|
-
metadata: nextState.metadata,
|
|
233
|
-
createdAt: new Date().toISOString(),
|
|
234
|
-
updatedAt: new Date().toISOString(),
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}, [history, historyIndex, dispatch]);
|
|
240
|
-
|
|
241
|
-
// Memoize the context value
|
|
242
|
-
const value = useMemo<EditorContextValue>(
|
|
243
|
-
() => ({
|
|
244
|
-
state,
|
|
245
|
-
dispatch,
|
|
246
|
-
darkMode,
|
|
247
|
-
backgroundColors,
|
|
248
|
-
helpers: {
|
|
249
|
-
addBlock,
|
|
250
|
-
updateBlock,
|
|
251
|
-
deleteBlock,
|
|
252
|
-
duplicateBlock,
|
|
253
|
-
moveBlock,
|
|
254
|
-
loadNewsletter,
|
|
255
|
-
resetEditor,
|
|
256
|
-
save,
|
|
257
|
-
undo,
|
|
258
|
-
redo,
|
|
259
|
-
},
|
|
260
|
-
canUndo: historyIndex > 0 && history.length > 0,
|
|
261
|
-
canRedo: historyIndex < history.length - 1,
|
|
262
|
-
}),
|
|
263
|
-
[state, dispatch, darkMode, backgroundColors, addBlock, updateBlock, deleteBlock, duplicateBlock, moveBlock, loadNewsletter, resetEditor, save, undo, redo, historyIndex, history.length]
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
return <EditorContext.Provider value={value}>{children}</EditorContext.Provider>;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Hook to access editor context
|
|
271
|
-
* @throws Error if used outside EditorProvider
|
|
272
|
-
*/
|
|
273
|
-
export function useEditor(): EditorContextValue {
|
|
274
|
-
const context = useContext(EditorContext);
|
|
275
|
-
if (!context) {
|
|
276
|
-
throw new Error('useEditor must be used within an EditorProvider');
|
|
277
|
-
}
|
|
278
|
-
return context;
|
|
279
|
-
}
|
package/src/state/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* State Exports
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export { EditorProvider, useEditor } from './EditorContext';
|
|
6
|
-
export type { EditorProviderProps } from './EditorContext';
|
|
7
|
-
export type { EditorContextValue } from './types';
|
|
8
|
-
export { editorReducer } from './reducer';
|
|
9
|
-
export type { EditorState, EditorAction } from './types';
|
|
10
|
-
export { initialEditorState } from './types';
|