@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,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;
@@ -1,5 +0,0 @@
1
- /**
2
- * Registry Exports
3
- */
4
-
5
- export { blockRegistry } from './BlockRegistry';
@@ -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
- }
@@ -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';