@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.
- package/package.json +8 -9
- 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
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { Search } from 'lucide-react';
|
|
5
|
-
import type { BlockTypeDefinition } from '../../../types/block';
|
|
6
|
-
|
|
7
|
-
export interface SlashCommandMenuProps {
|
|
8
|
-
/** Registered blocks to show in menu */
|
|
9
|
-
blocks: BlockTypeDefinition[];
|
|
10
|
-
/** Current search query */
|
|
11
|
-
query: string;
|
|
12
|
-
/** Selected index */
|
|
13
|
-
selectedIndex: number;
|
|
14
|
-
/** Callback when a block is selected - receives replaceBlockId if replacing */
|
|
15
|
-
onSelect: (replaceBlockId?: string) => void;
|
|
16
|
-
/** Position for the menu */
|
|
17
|
-
position: { top: number; left: number } | null;
|
|
18
|
-
/** Callback to close the menu */
|
|
19
|
-
onClose: () => void;
|
|
20
|
-
/** Block ID to replace (if replacing existing block) */
|
|
21
|
-
replaceBlockId?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function SlashCommandMenu({
|
|
25
|
-
blocks,
|
|
26
|
-
query,
|
|
27
|
-
selectedIndex,
|
|
28
|
-
onSelect,
|
|
29
|
-
position,
|
|
30
|
-
onClose,
|
|
31
|
-
replaceBlockId,
|
|
32
|
-
}: SlashCommandMenuProps) {
|
|
33
|
-
const menuRef = useRef<HTMLDivElement>(null);
|
|
34
|
-
|
|
35
|
-
// Handle keyboard navigation
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
38
|
-
if (e.key === 'ArrowDown') {
|
|
39
|
-
e.preventDefault();
|
|
40
|
-
// Selection is handled by parent
|
|
41
|
-
} else if (e.key === 'ArrowUp') {
|
|
42
|
-
e.preventDefault();
|
|
43
|
-
// Selection is handled by parent
|
|
44
|
-
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
45
|
-
e.preventDefault();
|
|
46
|
-
if (blocks[selectedIndex]) {
|
|
47
|
-
onSelect(replaceBlockId);
|
|
48
|
-
}
|
|
49
|
-
} else if (e.key === 'Escape') {
|
|
50
|
-
e.preventDefault();
|
|
51
|
-
onClose();
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
56
|
-
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
57
|
-
}, [blocks, selectedIndex, onSelect, onClose, replaceBlockId]);
|
|
58
|
-
|
|
59
|
-
// Scroll selected item into view
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
if (menuRef.current && selectedIndex >= 0) {
|
|
62
|
-
const selectedElement = menuRef.current.children[selectedIndex] as HTMLElement;
|
|
63
|
-
if (selectedElement) {
|
|
64
|
-
selectedElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}, [selectedIndex]);
|
|
68
|
-
|
|
69
|
-
if (!position || blocks.length === 0) {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<div
|
|
75
|
-
ref={menuRef}
|
|
76
|
-
className="fixed z-50 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-2xl overflow-hidden"
|
|
77
|
-
style={{
|
|
78
|
-
top: `${position.top}px`,
|
|
79
|
-
left: `${position.left}px`,
|
|
80
|
-
width: '280px',
|
|
81
|
-
maxHeight: '320px',
|
|
82
|
-
overflowY: 'auto',
|
|
83
|
-
}}
|
|
84
|
-
>
|
|
85
|
-
<div className="p-2 border-b border-neutral-200 dark:border-neutral-700 flex items-center gap-2">
|
|
86
|
-
<Search size={14} className="text-neutral-400" />
|
|
87
|
-
<input
|
|
88
|
-
type="text"
|
|
89
|
-
value={query}
|
|
90
|
-
readOnly
|
|
91
|
-
placeholder="Search blocks..."
|
|
92
|
-
className="flex-1 bg-transparent border-none outline-none text-sm text-neutral-600 dark:text-neutral-300 placeholder:text-neutral-400"
|
|
93
|
-
/>
|
|
94
|
-
</div>
|
|
95
|
-
<div className="py-1">
|
|
96
|
-
{blocks.map((block, index) => {
|
|
97
|
-
const IconComponent = block.icon || block.components.Icon;
|
|
98
|
-
const isSelected = index === selectedIndex;
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<button
|
|
102
|
-
key={block.type}
|
|
103
|
-
onClick={() => onSelect(replaceBlockId)}
|
|
104
|
-
className={`w-full px-3 py-2 flex items-center gap-3 text-left transition-colors ${
|
|
105
|
-
isSelected
|
|
106
|
-
? 'bg-primary/10 text-primary dark:bg-primary/20'
|
|
107
|
-
: 'hover:bg-neutral-100 dark:hover:bg-neutral-700 text-neutral-700 dark:text-neutral-300'
|
|
108
|
-
}`}
|
|
109
|
-
>
|
|
110
|
-
{IconComponent && (
|
|
111
|
-
<div className={`flex-shrink-0 ${isSelected ? 'text-primary' : 'text-neutral-400'}`}>
|
|
112
|
-
<IconComponent />
|
|
113
|
-
</div>
|
|
114
|
-
)}
|
|
115
|
-
<div className="flex-1 min-w-0">
|
|
116
|
-
<div className={`text-sm font-medium ${isSelected ? 'text-primary' : 'text-neutral-900 dark:text-neutral-100'}`}>
|
|
117
|
-
{block.name}
|
|
118
|
-
</div>
|
|
119
|
-
{block.description && (
|
|
120
|
-
<div className="text-xs text-neutral-500 dark:text-neutral-400 truncate">
|
|
121
|
-
{block.description}
|
|
122
|
-
</div>
|
|
123
|
-
)}
|
|
124
|
-
</div>
|
|
125
|
-
</button>
|
|
126
|
-
);
|
|
127
|
-
})}
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Newsletter Editor Components Exports
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export { ErrorBanner } from './ErrorBanner';
|
|
6
|
-
export type { ErrorBannerProps } from './ErrorBanner';
|
|
7
|
-
export { LibraryItem } from './LibraryItem';
|
|
8
|
-
export type { LibraryItemProps } from './LibraryItem';
|
|
9
|
-
export { CustomBlockItem } from './CustomBlockItem';
|
|
10
|
-
export type { CustomBlockItemProps } from './CustomBlockItem';
|
|
11
|
-
export { EditorLibrary } from './EditorLibrary';
|
|
12
|
-
export type { EditorLibraryProps } from './EditorLibrary';
|
|
13
|
-
export { EditorCanvas } from './EditorCanvas';
|
|
14
|
-
export type { EditorCanvasProps } from './EditorCanvas';
|
|
15
|
-
export { EditorSidebar } from './EditorSidebar';
|
|
16
|
-
export type { EditorSidebarProps } from './EditorSidebar';
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
2
|
-
import type { Block } from '../../../types/block';
|
|
3
|
-
import type { EditorState } from '../../../state/types';
|
|
4
|
-
|
|
5
|
-
// Generate a unique block ID
|
|
6
|
-
function generateBlockId(): string {
|
|
7
|
-
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
8
|
-
return crypto.randomUUID();
|
|
9
|
-
}
|
|
10
|
-
return `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function useKeyboardShortcuts(
|
|
14
|
-
state: EditorState,
|
|
15
|
-
dispatch: (action: any) => void,
|
|
16
|
-
canUndo: boolean,
|
|
17
|
-
canRedo: boolean,
|
|
18
|
-
undo: () => void,
|
|
19
|
-
redo: () => void
|
|
20
|
-
) {
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
23
|
-
// Don't handle shortcuts if user is typing in an input/textarea/contentEditable
|
|
24
|
-
const target = e.target as HTMLElement;
|
|
25
|
-
const isEditableElement = target.tagName === 'INPUT' ||
|
|
26
|
-
target.tagName === 'TEXTAREA' ||
|
|
27
|
-
target.isContentEditable ||
|
|
28
|
-
target.closest('input, textarea, [contenteditable="true"]');
|
|
29
|
-
|
|
30
|
-
// Check for Ctrl+Z / Cmd+Z (Undo)
|
|
31
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
32
|
-
if (!isEditableElement) {
|
|
33
|
-
e.preventDefault();
|
|
34
|
-
e.stopPropagation();
|
|
35
|
-
if (canUndo) {
|
|
36
|
-
undo();
|
|
37
|
-
}
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Check for Ctrl+Shift+Z / Cmd+Shift+Z or Ctrl+Y / Cmd+Y (Redo)
|
|
43
|
-
if (((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'z') || ((e.ctrlKey || e.metaKey) && e.key === 'y')) {
|
|
44
|
-
if (!isEditableElement) {
|
|
45
|
-
e.preventDefault();
|
|
46
|
-
e.stopPropagation();
|
|
47
|
-
if (canRedo) {
|
|
48
|
-
redo();
|
|
49
|
-
}
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Check for Ctrl+V (Windows/Linux) or Cmd+V (Mac)
|
|
55
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
|
|
56
|
-
// Don't paste if user is typing in an input/textarea/contentEditable
|
|
57
|
-
if (isEditableElement) {
|
|
58
|
-
return; // Let the browser handle paste in editable elements
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Check if there's a copied block
|
|
62
|
-
if (typeof window !== 'undefined') {
|
|
63
|
-
const copiedBlockJson = localStorage.getItem('__NEWSLETTER_EDITOR_COPIED_BLOCK__');
|
|
64
|
-
if (copiedBlockJson) {
|
|
65
|
-
try {
|
|
66
|
-
e.preventDefault();
|
|
67
|
-
e.stopPropagation();
|
|
68
|
-
|
|
69
|
-
const copiedBlock = JSON.parse(copiedBlockJson) as Block;
|
|
70
|
-
|
|
71
|
-
// Clone a block with new IDs (recursive for nested blocks)
|
|
72
|
-
const cloneBlock = (blockToClone: Block): Block => {
|
|
73
|
-
const cloned: Block = {
|
|
74
|
-
...blockToClone,
|
|
75
|
-
id: generateBlockId(),
|
|
76
|
-
data: { ...blockToClone.data },
|
|
77
|
-
meta: blockToClone.meta ? { ...blockToClone.meta } : undefined,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// Handle children if they exist
|
|
81
|
-
if (blockToClone.children) {
|
|
82
|
-
if (Array.isArray(blockToClone.children) && blockToClone.children.length > 0) {
|
|
83
|
-
if (typeof blockToClone.children[0] === 'object') {
|
|
84
|
-
cloned.children = (blockToClone.children as Block[]).map(cloneBlock);
|
|
85
|
-
} else {
|
|
86
|
-
// If children are IDs, find and clone the actual blocks
|
|
87
|
-
const allBlocks = state.blocks;
|
|
88
|
-
const childIds = blockToClone.children as string[];
|
|
89
|
-
const childBlocks = childIds
|
|
90
|
-
.map((childId: string) => allBlocks.find(b => b.id === childId))
|
|
91
|
-
.filter((b): b is Block => b !== undefined);
|
|
92
|
-
cloned.children = childBlocks.map(cloneBlock);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return cloned;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const pastedBlock = cloneBlock(copiedBlock);
|
|
101
|
-
|
|
102
|
-
// Find where to paste - use hovered block or selected block, or paste at end
|
|
103
|
-
const hoveredBlockId = (window as any).__NEWSLETTER_EDITOR_HOVERED_BLOCK_ID__;
|
|
104
|
-
const targetBlockId = hoveredBlockId || state.selectedBlockId;
|
|
105
|
-
|
|
106
|
-
let pasteIndex: number | undefined;
|
|
107
|
-
if (targetBlockId) {
|
|
108
|
-
const targetIndex = state.blocks.findIndex(b => b.id === targetBlockId);
|
|
109
|
-
if (targetIndex !== -1) {
|
|
110
|
-
pasteIndex = targetIndex + 1;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Dispatch ADD_BLOCK with the full block structure
|
|
115
|
-
dispatch({
|
|
116
|
-
type: 'ADD_BLOCK',
|
|
117
|
-
payload: {
|
|
118
|
-
block: pastedBlock,
|
|
119
|
-
index: pasteIndex,
|
|
120
|
-
containerId: undefined
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
} catch (error) {
|
|
124
|
-
console.error('Failed to paste block:', error);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
132
|
-
return () => {
|
|
133
|
-
window.removeEventListener('keydown', handleKeyDown);
|
|
134
|
-
};
|
|
135
|
-
}, [state.blocks, state.selectedBlockId, dispatch, canUndo, canRedo, undo, redo]);
|
|
136
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
import { Newsletter } from '../../../types/newsletter';
|
|
3
|
-
|
|
4
|
-
export function useNewsletterLoader(
|
|
5
|
-
newsletterSlug: string | undefined,
|
|
6
|
-
currentNewsletterId: string | null,
|
|
7
|
-
loadNewsletter: (newsletter: Newsletter) => void
|
|
8
|
-
) {
|
|
9
|
-
const [isLoadingNewsletter, setIsLoadingNewsletter] = useState(false);
|
|
10
|
-
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
if (newsletterSlug && !currentNewsletterId) {
|
|
13
|
-
const loadNewsletterData = async () => {
|
|
14
|
-
try {
|
|
15
|
-
setIsLoadingNewsletter(true);
|
|
16
|
-
const response = await fetch(`/api/plugin-newsletter/newsletters/${newsletterSlug}`);
|
|
17
|
-
if (!response.ok) {
|
|
18
|
-
throw new Error('Failed to load newsletter');
|
|
19
|
-
}
|
|
20
|
-
const newsletter: Newsletter = await response.json();
|
|
21
|
-
loadNewsletter(newsletter);
|
|
22
|
-
} catch (error) {
|
|
23
|
-
console.error('Failed to load newsletter:', error);
|
|
24
|
-
alert('Failed to load newsletter. Please try again.');
|
|
25
|
-
} finally {
|
|
26
|
-
setIsLoadingNewsletter(false);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
loadNewsletterData();
|
|
30
|
-
}
|
|
31
|
-
}, [newsletterSlug, currentNewsletterId, loadNewsletter]);
|
|
32
|
-
|
|
33
|
-
return { isLoadingNewsletter };
|
|
34
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
2
|
-
import { blockRegistry } from '../../../registry/BlockRegistry';
|
|
3
|
-
|
|
4
|
-
export function useRegisteredBlocks() {
|
|
5
|
-
const [registeredBlocks, setRegisteredBlocks] = useState(() => {
|
|
6
|
-
const initial = blockRegistry.getAll();
|
|
7
|
-
return initial;
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
// Watch for registry changes and update state
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
// Check immediately
|
|
13
|
-
const checkBlocks = () => {
|
|
14
|
-
const currentBlocks = blockRegistry.getAll();
|
|
15
|
-
const hasChanged = currentBlocks.length !== registeredBlocks.length ||
|
|
16
|
-
currentBlocks.some((b, i) => b.type !== registeredBlocks[i]?.type) ||
|
|
17
|
-
registeredBlocks.some((b, i) => b.type !== currentBlocks[i]?.type);
|
|
18
|
-
|
|
19
|
-
if (hasChanged) {
|
|
20
|
-
setRegisteredBlocks([...currentBlocks]);
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// Initial check
|
|
25
|
-
checkBlocks();
|
|
26
|
-
|
|
27
|
-
// Poll for registry changes (blocks are registered asynchronously in useEffect)
|
|
28
|
-
let pollCount = 0;
|
|
29
|
-
const interval = setInterval(() => {
|
|
30
|
-
pollCount++;
|
|
31
|
-
checkBlocks();
|
|
32
|
-
// Stop polling after 5 seconds (25 checks at 200ms)
|
|
33
|
-
if (pollCount > 25) {
|
|
34
|
-
clearInterval(interval);
|
|
35
|
-
}
|
|
36
|
-
}, 200);
|
|
37
|
-
|
|
38
|
-
// Also check after delays to catch initial registrations
|
|
39
|
-
const timeouts = [
|
|
40
|
-
setTimeout(checkBlocks, 50),
|
|
41
|
-
setTimeout(checkBlocks, 100),
|
|
42
|
-
setTimeout(checkBlocks, 300),
|
|
43
|
-
setTimeout(checkBlocks, 500),
|
|
44
|
-
setTimeout(checkBlocks, 1000),
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
return () => {
|
|
48
|
-
clearInterval(interval);
|
|
49
|
-
timeouts.forEach(clearTimeout);
|
|
50
|
-
};
|
|
51
|
-
}, [registeredBlocks.length]);
|
|
52
|
-
|
|
53
|
-
return registeredBlocks;
|
|
54
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import type { BlockTypeDefinition } from '../../../types/block';
|
|
3
|
-
|
|
4
|
-
export interface SlashCommandState {
|
|
5
|
-
isOpen: boolean;
|
|
6
|
-
query: string;
|
|
7
|
-
selectedIndex: number;
|
|
8
|
-
position: { top: number; left: number } | null;
|
|
9
|
-
replaceBlockId?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function useSlashCommand(
|
|
13
|
-
blocks: BlockTypeDefinition[],
|
|
14
|
-
onSelectBlock: (blockType: string, replaceBlockId?: string) => void
|
|
15
|
-
) {
|
|
16
|
-
const [state, setState] = useState<SlashCommandState>({
|
|
17
|
-
isOpen: false,
|
|
18
|
-
query: '',
|
|
19
|
-
selectedIndex: 0,
|
|
20
|
-
position: null,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// Filter blocks based on query
|
|
24
|
-
const filteredBlocks = blocks.filter(block => {
|
|
25
|
-
if (!state.query) return true;
|
|
26
|
-
const lowerQuery = state.query.toLowerCase();
|
|
27
|
-
const name = block.name.toLowerCase();
|
|
28
|
-
const description = block.description?.toLowerCase() || '';
|
|
29
|
-
const type = block.type.toLowerCase();
|
|
30
|
-
return name.includes(lowerQuery) || description.includes(lowerQuery) || type.includes(lowerQuery);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const openMenu = useCallback((position: { top: number; left: number }, replaceBlockId?: string) => {
|
|
34
|
-
setState({
|
|
35
|
-
isOpen: true,
|
|
36
|
-
query: '',
|
|
37
|
-
selectedIndex: 0,
|
|
38
|
-
position,
|
|
39
|
-
replaceBlockId,
|
|
40
|
-
});
|
|
41
|
-
}, []);
|
|
42
|
-
|
|
43
|
-
const closeMenu = useCallback(() => {
|
|
44
|
-
setState({
|
|
45
|
-
isOpen: false,
|
|
46
|
-
query: '',
|
|
47
|
-
selectedIndex: 0,
|
|
48
|
-
position: null,
|
|
49
|
-
replaceBlockId: undefined,
|
|
50
|
-
});
|
|
51
|
-
}, []);
|
|
52
|
-
|
|
53
|
-
const updateQuery = useCallback((query: string) => {
|
|
54
|
-
setState(prev => ({
|
|
55
|
-
...prev,
|
|
56
|
-
query,
|
|
57
|
-
selectedIndex: 0, // Reset selection when query changes
|
|
58
|
-
}));
|
|
59
|
-
}, []);
|
|
60
|
-
|
|
61
|
-
const moveSelection = useCallback((direction: 'up' | 'down') => {
|
|
62
|
-
setState(prev => {
|
|
63
|
-
if (!prev.isOpen || filteredBlocks.length === 0) return prev;
|
|
64
|
-
|
|
65
|
-
let newIndex = prev.selectedIndex;
|
|
66
|
-
if (direction === 'down') {
|
|
67
|
-
newIndex = (prev.selectedIndex + 1) % filteredBlocks.length;
|
|
68
|
-
} else {
|
|
69
|
-
newIndex = prev.selectedIndex === 0 ? filteredBlocks.length - 1 : prev.selectedIndex - 1;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
...prev,
|
|
74
|
-
selectedIndex: newIndex,
|
|
75
|
-
};
|
|
76
|
-
});
|
|
77
|
-
}, [filteredBlocks.length]);
|
|
78
|
-
|
|
79
|
-
const selectCurrent = useCallback((replaceBlockId?: string) => {
|
|
80
|
-
if (state.isOpen && filteredBlocks[state.selectedIndex]) {
|
|
81
|
-
// Always use the replaceBlockId from state (set when menu was opened)
|
|
82
|
-
// The parameter is ignored to ensure we use the correct block ID
|
|
83
|
-
const blockIdToReplace = state.replaceBlockId;
|
|
84
|
-
if (!blockIdToReplace) {
|
|
85
|
-
console.warn('[useSlashCommand] No replaceBlockId in state when selecting block');
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
onSelectBlock(filteredBlocks[state.selectedIndex].type, blockIdToReplace);
|
|
89
|
-
closeMenu();
|
|
90
|
-
}
|
|
91
|
-
}, [state.isOpen, state.selectedIndex, state.replaceBlockId, filteredBlocks, onSelectBlock, closeMenu]);
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
isOpen: state.isOpen,
|
|
95
|
-
query: state.query,
|
|
96
|
-
selectedIndex: state.selectedIndex,
|
|
97
|
-
position: state.position,
|
|
98
|
-
replaceBlockId: state.replaceBlockId,
|
|
99
|
-
filteredBlocks,
|
|
100
|
-
openMenu,
|
|
101
|
-
closeMenu,
|
|
102
|
-
updateQuery,
|
|
103
|
-
moveSelection,
|
|
104
|
-
selectCurrent,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Newsletter Canvas Editor Exports
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export { CanvasEditorView } from './CanvasEditorView';
|
|
6
|
-
export type { CanvasEditorViewProps } from './CanvasEditorView';
|
|
7
|
-
export { EditorBody } from './EditorBody';
|
|
8
|
-
export type { EditorBodyProps } from './EditorBody';
|
|
9
|
-
export { BlockWrapper } from './BlockWrapper';
|
|
10
|
-
export type { BlockWrapperProps } from './BlockWrapper';
|
|
11
|
-
export { EditorHeader } from './EditorHeader';
|
|
12
|
-
export type { EditorHeaderProps } from './EditorHeader';
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Newsletter Editor View
|
|
3
|
-
* Block-based editor for creating and editing newsletters
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use client';
|
|
7
|
-
|
|
8
|
-
import React from 'react';
|
|
9
|
-
import { CanvasEditorView } from './CanvasEditor';
|
|
10
|
-
|
|
11
|
-
export interface NewsletterEditorViewProps {
|
|
12
|
-
newsletterSlug?: string;
|
|
13
|
-
siteId: string;
|
|
14
|
-
locale: string;
|
|
15
|
-
darkMode?: boolean;
|
|
16
|
-
backgroundColors?: {
|
|
17
|
-
light: string;
|
|
18
|
-
dark?: string;
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function NewsletterEditorView({
|
|
23
|
-
newsletterSlug,
|
|
24
|
-
siteId,
|
|
25
|
-
locale,
|
|
26
|
-
darkMode,
|
|
27
|
-
backgroundColors
|
|
28
|
-
}: NewsletterEditorViewProps) {
|
|
29
|
-
return (
|
|
30
|
-
<CanvasEditorView
|
|
31
|
-
newsletterSlug={newsletterSlug}
|
|
32
|
-
siteId={siteId}
|
|
33
|
-
locale={locale}
|
|
34
|
-
darkMode={darkMode}
|
|
35
|
-
backgroundColors={backgroundColors}
|
|
36
|
-
/>
|
|
37
|
-
);
|
|
38
|
-
}
|