hazo_notify 1.0.0

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 (61) hide show
  1. package/.cursor/rules/db_schema.mdc +0 -0
  2. package/.cursor/rules/design.mdc +16 -0
  3. package/.cursor/rules/general.mdc +49 -0
  4. package/README.md +765 -0
  5. package/components/emailer-html-editor.tsx +94 -0
  6. package/components/ui/button.tsx +53 -0
  7. package/components/ui/card.tsx +78 -0
  8. package/components/ui/input.tsx +24 -0
  9. package/components/ui/label.tsx +21 -0
  10. package/components/ui/sidebar.tsx +121 -0
  11. package/components/ui/spinner.tsx +54 -0
  12. package/components/ui/textarea.tsx +23 -0
  13. package/components/ui/tooltip.tsx +30 -0
  14. package/components.json +20 -0
  15. package/hazo_notify_config.ini +153 -0
  16. package/jest.config.js +27 -0
  17. package/jest.setup.js +1 -0
  18. package/next.config.js +22 -0
  19. package/package.json +72 -0
  20. package/postcss.config.js +6 -0
  21. package/src/app/api/hazo_notify/emailer/send/__tests__/route.test.ts +227 -0
  22. package/src/app/api/hazo_notify/emailer/send/route.ts +537 -0
  23. package/src/app/editor-00/page.tsx +47 -0
  24. package/src/app/globals.css +69 -0
  25. package/src/app/hazo_notify/emailer_test/layout.tsx +53 -0
  26. package/src/app/hazo_notify/emailer_test/page.tsx +369 -0
  27. package/src/app/hazo_notify/layout.tsx +77 -0
  28. package/src/app/hazo_notify/page.tsx +12 -0
  29. package/src/app/layout.tsx +26 -0
  30. package/src/app/page.tsx +14 -0
  31. package/src/components/blocks/editor-00/editor.tsx +61 -0
  32. package/src/components/blocks/editor-00/nodes.ts +11 -0
  33. package/src/components/blocks/editor-00/plugins.tsx +36 -0
  34. package/src/components/editor/editor-ui/content-editable.tsx +34 -0
  35. package/src/components/editor/themes/editor-theme.css +91 -0
  36. package/src/components/editor/themes/editor-theme.ts +130 -0
  37. package/src/components/ui/button.tsx +53 -0
  38. package/src/components/ui/card.tsx +78 -0
  39. package/src/components/ui/input.tsx +24 -0
  40. package/src/components/ui/label.tsx +21 -0
  41. package/src/components/ui/sidebar.tsx +121 -0
  42. package/src/components/ui/spinner.tsx +54 -0
  43. package/src/components/ui/textarea.tsx +23 -0
  44. package/src/components/ui/tooltip.tsx +30 -0
  45. package/src/lib/emailer/__tests__/emailer.test.ts +200 -0
  46. package/src/lib/emailer/emailer.ts +263 -0
  47. package/src/lib/emailer/index.ts +11 -0
  48. package/src/lib/emailer/providers/__tests__/zeptomail_provider.test.ts +196 -0
  49. package/src/lib/emailer/providers/index.ts +33 -0
  50. package/src/lib/emailer/providers/pop3_provider.ts +30 -0
  51. package/src/lib/emailer/providers/smtp_provider.ts +30 -0
  52. package/src/lib/emailer/providers/zeptomail_provider.ts +299 -0
  53. package/src/lib/emailer/types.ts +119 -0
  54. package/src/lib/emailer/utils/constants.ts +24 -0
  55. package/src/lib/emailer/utils/index.ts +9 -0
  56. package/src/lib/emailer/utils/logger.ts +71 -0
  57. package/src/lib/emailer/utils/validation.ts +84 -0
  58. package/src/lib/index.ts +6 -0
  59. package/src/lib/utils.ts +6 -0
  60. package/tailwind.config.ts +65 -0
  61. package/tsconfig.json +27 -0
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Emailer test UI page
3
+ * Allows testing email functionality with a form interface
4
+ * Displays raw output and response received
5
+ * Layout: Grid with 3 rows
6
+ * - Row 1: Header row with general information
7
+ * - Row 2: Two columns (email input fields + send button | response output)
8
+ * - Row 3: Email response
9
+ * - Bottom: Raw message to be sent
10
+ */
11
+
12
+ 'use client';
13
+
14
+ import { useState } from 'react';
15
+ import { Button } from '@/components/ui/button';
16
+ import { Input } from '@/components/ui/input';
17
+ import { Textarea } from '@/components/ui/textarea';
18
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
19
+ import { Label } from '@/components/ui/label';
20
+ import { Spinner } from '@/components/ui/spinner';
21
+ import { toast } from 'sonner';
22
+ import { Send, CheckCircle2, XCircle, Mail, Info } from 'lucide-react';
23
+
24
+ interface EmailResponse {
25
+ success: boolean;
26
+ message_id?: string;
27
+ message?: string;
28
+ raw_response?: any;
29
+ error?: string;
30
+ }
31
+
32
+ export default function EmailerTestPage() {
33
+ const [to_email, set_to_email] = useState('');
34
+ const [subject, set_subject] = useState('');
35
+ const [text_body, set_text_body] = useState('');
36
+ const [html_body, set_html_body] = useState('');
37
+ const [is_loading, set_is_loading] = useState(false);
38
+ const [response, set_response] = useState<EmailResponse | null>(null);
39
+ const [raw_output, set_raw_output] = useState<any>(null);
40
+
41
+ /**
42
+ * Handle form submission
43
+ */
44
+ const handle_submit = async (e: React.FormEvent) => {
45
+ e.preventDefault();
46
+
47
+ // Validate form
48
+ if (!to_email) {
49
+ toast.error('Recipient email address is required');
50
+ return;
51
+ }
52
+
53
+ if (!subject) {
54
+ toast.error('Email subject is required');
55
+ return;
56
+ }
57
+
58
+ if (!text_body && !html_body) {
59
+ toast.error('Email content (text or HTML) is required');
60
+ return;
61
+ }
62
+
63
+ set_is_loading(true);
64
+ set_response(null);
65
+ set_raw_output(null);
66
+
67
+ try {
68
+ // Convert newlines to <br> tags in HTML body
69
+ const processed_html_body = html_body
70
+ ? html_body.replace(/\n/g, '<br>')
71
+ : undefined;
72
+
73
+ // Prepare request body
74
+ const request_body = {
75
+ to: to_email,
76
+ subject: subject,
77
+ content: {
78
+ text: text_body || undefined,
79
+ html: processed_html_body,
80
+ },
81
+ };
82
+
83
+ // Store raw output (request) - shows the processed request that will be sent
84
+ set_raw_output({
85
+ method: 'POST',
86
+ url: '/api/hazo_notify/emailer/send',
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ },
90
+ body: request_body,
91
+ });
92
+
93
+ // Make API request
94
+ const api_response = await fetch('/api/hazo_notify/emailer/send', {
95
+ method: 'POST',
96
+ headers: {
97
+ 'Content-Type': 'application/json',
98
+ },
99
+ body: JSON.stringify(request_body),
100
+ });
101
+
102
+ const response_data = await api_response.json();
103
+ set_response(response_data);
104
+
105
+ // Show toast notification
106
+ if (response_data.success) {
107
+ toast.success('Email sent successfully!');
108
+ } else {
109
+ toast.error(response_data.error || 'Failed to send email');
110
+ }
111
+ } catch (error: any) {
112
+ const error_message = error?.message || 'Failed to send email';
113
+ set_response({
114
+ success: false,
115
+ error: error_message,
116
+ message: error_message,
117
+ });
118
+ toast.error(error_message);
119
+ } finally {
120
+ set_is_loading(false);
121
+ }
122
+ };
123
+
124
+ return (
125
+ <div className="cls_emailer_test_container h-full w-full p-6">
126
+ {/* Row 1: Header row with general information */}
127
+ <div className="cls_emailer_test_row_1 mb-6">
128
+ <Card className="cls_emailer_test_header_card">
129
+ <CardHeader className="cls_emailer_test_header_card_header">
130
+ <div className="cls_emailer_test_header_title_section flex items-center gap-3">
131
+ <Mail className="cls_emailer_test_header_icon h-6 w-6" />
132
+ <CardTitle className="cls_emailer_test_header_title text-2xl">
133
+ Email Tester
134
+ </CardTitle>
135
+ </div>
136
+ <CardDescription className="cls_emailer_test_header_description mt-2">
137
+ Test email functionality by sending test emails. Configure your email service in hazo_notify_config.ini
138
+ </CardDescription>
139
+ </CardHeader>
140
+ <CardContent className="cls_emailer_test_header_card_content">
141
+ <div className="cls_emailer_test_header_info grid grid-cols-1 md:grid-cols-3 gap-4">
142
+ <div className="cls_emailer_test_header_info_item flex items-center gap-2">
143
+ <Info className="cls_emailer_test_header_info_icon h-4 w-4 text-muted-foreground" />
144
+ <span className="cls_emailer_test_header_info_text text-sm text-muted-foreground">
145
+ Status: {response ? (response.success ? 'Success' : 'Failed') : 'Ready'}
146
+ </span>
147
+ </div>
148
+ <div className="cls_emailer_test_header_info_item flex items-center gap-2">
149
+ <Info className="cls_emailer_test_header_info_icon h-4 w-4 text-muted-foreground" />
150
+ <span className="cls_emailer_test_header_info_text text-sm text-muted-foreground">
151
+ Provider: Zeptomail API
152
+ </span>
153
+ </div>
154
+ <div className="cls_emailer_test_header_info_item flex items-center gap-2">
155
+ <Info className="cls_emailer_test_header_info_icon h-4 w-4 text-muted-foreground" />
156
+ <span className="cls_emailer_test_header_info_text text-sm text-muted-foreground">
157
+ {response?.message_id ? `Message ID: ${response.message_id}` : 'No message sent yet'}
158
+ </span>
159
+ </div>
160
+ </div>
161
+ </CardContent>
162
+ </Card>
163
+ </div>
164
+
165
+ {/* Row 2: Two columns - email input fields + send button | response output */}
166
+ <div className="cls_emailer_test_row_2 grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
167
+ {/* Column 1: Email input fields and send button */}
168
+ <Card className="cls_emailer_test_input_card">
169
+ <CardHeader className="cls_emailer_test_input_card_header">
170
+ <CardTitle className="cls_emailer_test_input_card_title">Email Configuration</CardTitle>
171
+ <CardDescription className="cls_emailer_test_input_card_description">
172
+ Enter email details to send a test email
173
+ </CardDescription>
174
+ </CardHeader>
175
+ <CardContent className="cls_emailer_test_input_card_content">
176
+ <form onSubmit={handle_submit} className="cls_emailer_test_form space-y-4">
177
+ <div className="cls_emailer_test_field_to space-y-2">
178
+ <Label htmlFor="to_email" className="cls_emailer_test_label">
179
+ To Email
180
+ </Label>
181
+ <Input
182
+ id="to_email"
183
+ type="email"
184
+ placeholder="recipient@example.com"
185
+ value={to_email}
186
+ onChange={(e) => set_to_email(e.target.value)}
187
+ disabled={is_loading}
188
+ className="cls_emailer_test_input_to"
189
+ required
190
+ />
191
+ </div>
192
+
193
+ <div className="cls_emailer_test_field_subject space-y-2">
194
+ <Label htmlFor="subject" className="cls_emailer_test_label">
195
+ Subject
196
+ </Label>
197
+ <Input
198
+ id="subject"
199
+ type="text"
200
+ placeholder="Email subject"
201
+ value={subject}
202
+ onChange={(e) => set_subject(e.target.value)}
203
+ disabled={is_loading}
204
+ className="cls_emailer_test_input_subject"
205
+ required
206
+ />
207
+ </div>
208
+
209
+ <div className="cls_emailer_test_field_text space-y-2">
210
+ <Label htmlFor="text_body" className="cls_emailer_test_label">
211
+ Text Body (optional)
212
+ </Label>
213
+ <Textarea
214
+ id="text_body"
215
+ placeholder="Plain text email content"
216
+ value={text_body}
217
+ onChange={(e) => set_text_body(e.target.value)}
218
+ disabled={is_loading}
219
+ className="cls_emailer_test_textarea_text min-h-[120px]"
220
+ rows={5}
221
+ />
222
+ </div>
223
+
224
+ <div className="cls_emailer_test_field_html space-y-2">
225
+ <Label htmlFor="html_body" className="cls_emailer_test_label">
226
+ HTML Body (optional)
227
+ </Label>
228
+ <Textarea
229
+ id="html_body"
230
+ placeholder="HTML email content"
231
+ value={html_body}
232
+ onChange={(e) => set_html_body(e.target.value)}
233
+ disabled={is_loading}
234
+ className="cls_emailer_test_textarea_html min-h-[120px]"
235
+ rows={5}
236
+ />
237
+ </div>
238
+
239
+ <div className="cls_emailer_test_actions flex gap-2 pt-4">
240
+ <Button
241
+ type="submit"
242
+ disabled={is_loading}
243
+ className="cls_emailer_test_button_send"
244
+ >
245
+ {is_loading ? (
246
+ <>
247
+ <Spinner size="sm" className="mr-2" />
248
+ Sending...
249
+ </>
250
+ ) : (
251
+ <>
252
+ <Send className="mr-2 h-4 w-4" />
253
+ Send Email
254
+ </>
255
+ )}
256
+ </Button>
257
+ </div>
258
+ </form>
259
+ </CardContent>
260
+ </Card>
261
+
262
+ {/* Column 2: Response output */}
263
+ <Card className="cls_emailer_test_response_card">
264
+ <CardHeader className="cls_emailer_test_response_card_header">
265
+ <CardTitle className="cls_emailer_test_response_card_title">Response Output</CardTitle>
266
+ <CardDescription className="cls_emailer_test_response_card_description">
267
+ Response received from the email service
268
+ </CardDescription>
269
+ </CardHeader>
270
+ <CardContent className="cls_emailer_test_response_card_content">
271
+ {response ? (
272
+ <div className="cls_emailer_test_response_content space-y-4">
273
+ <div className="cls_emailer_test_response_status flex items-center gap-2">
274
+ {response.success ? (
275
+ <>
276
+ <CheckCircle2 className="h-5 w-5 text-green-500" />
277
+ <span className="cls_emailer_test_response_status_text text-green-600 font-medium">
278
+ Success
279
+ </span>
280
+ </>
281
+ ) : (
282
+ <>
283
+ <XCircle className="h-5 w-5 text-red-500" />
284
+ <span className="cls_emailer_test_response_status_text text-red-600 font-medium">
285
+ Failed
286
+ </span>
287
+ </>
288
+ )}
289
+ </div>
290
+ {response.message && (
291
+ <div className="cls_emailer_test_response_message">
292
+ <Label className="cls_emailer_test_response_message_label">Message:</Label>
293
+ <p className="cls_emailer_test_response_message_text text-sm mt-1">
294
+ {response.message}
295
+ </p>
296
+ </div>
297
+ )}
298
+ {response.message_id && (
299
+ <div className="cls_emailer_test_response_message_id">
300
+ <Label className="cls_emailer_test_response_message_id_label">Message ID:</Label>
301
+ <p className="cls_emailer_test_response_message_id_text text-sm mt-1 font-mono">
302
+ {response.message_id}
303
+ </p>
304
+ </div>
305
+ )}
306
+ {response.error && (
307
+ <div className="cls_emailer_test_response_error">
308
+ <Label className="cls_emailer_test_response_error_label text-red-600">Error:</Label>
309
+ <p className="cls_emailer_test_response_error_text text-sm mt-1 text-red-600">
310
+ {response.error}
311
+ </p>
312
+ </div>
313
+ )}
314
+ </div>
315
+ ) : (
316
+ <div className="cls_emailer_test_response_empty text-center text-muted-foreground py-8">
317
+ <p className="cls_emailer_test_response_empty_text">
318
+ No response yet. Send an email to see the response here.
319
+ </p>
320
+ </div>
321
+ )}
322
+ </CardContent>
323
+ </Card>
324
+ </div>
325
+
326
+ {/* Row 3: Email response details */}
327
+ {response && (
328
+ <div className="cls_emailer_test_row_3 mb-6">
329
+ <Card className="cls_emailer_test_response_details_card">
330
+ <CardHeader className="cls_emailer_test_response_details_card_header">
331
+ <CardTitle className="cls_emailer_test_response_details_card_title">Email Response Details</CardTitle>
332
+ <CardDescription className="cls_emailer_test_response_details_card_description">
333
+ Full response received from the email service
334
+ </CardDescription>
335
+ </CardHeader>
336
+ <CardContent className="cls_emailer_test_response_details_card_content">
337
+ {response.raw_response && (
338
+ <div className="cls_emailer_test_response_details_raw">
339
+ <pre className="cls_emailer_test_response_details_raw_pre bg-muted p-4 rounded-md overflow-auto text-xs">
340
+ {JSON.stringify(response.raw_response, null, 2)}
341
+ </pre>
342
+ </div>
343
+ )}
344
+ </CardContent>
345
+ </Card>
346
+ </div>
347
+ )}
348
+
349
+ {/* Bottom: Raw message to be sent */}
350
+ {raw_output && (
351
+ <div className="cls_emailer_test_bottom">
352
+ <Card className="cls_emailer_test_raw_message_card">
353
+ <CardHeader className="cls_emailer_test_raw_message_card_header">
354
+ <CardTitle className="cls_emailer_test_raw_message_card_title">Raw Message to be Sent</CardTitle>
355
+ <CardDescription className="cls_emailer_test_raw_message_card_description">
356
+ Complete request payload that will be sent to the email service
357
+ </CardDescription>
358
+ </CardHeader>
359
+ <CardContent className="cls_emailer_test_raw_message_card_content">
360
+ <pre className="cls_emailer_test_raw_message_pre bg-muted p-4 rounded-md overflow-auto text-xs">
361
+ {JSON.stringify(raw_output, null, 2)}
362
+ </pre>
363
+ </CardContent>
364
+ </Card>
365
+ </div>
366
+ )}
367
+ </div>
368
+ );
369
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Layout for hazo_notify services
3
+ * Includes sidebar navigation for all services
4
+ * Checks if emailer test UI is enabled in config
5
+ */
6
+
7
+ 'use client';
8
+
9
+ import { usePathname } from 'next/navigation';
10
+ import Link from 'next/link';
11
+ import {
12
+ Sidebar,
13
+ SidebarHeader,
14
+ SidebarContent,
15
+ SidebarNav,
16
+ SidebarNavItem,
17
+ SidebarNavLink,
18
+ } from '@/components/ui/sidebar';
19
+ import { Mail } from 'lucide-react';
20
+ import { cn } from '@/lib/utils';
21
+
22
+ interface ServiceItem {
23
+ title: string;
24
+ href: string;
25
+ icon: React.ReactNode;
26
+ }
27
+
28
+ const services: ServiceItem[] = [
29
+ {
30
+ title: 'Email Tester',
31
+ href: '/hazo_notify/emailer_test',
32
+ icon: <Mail className="h-4 w-4" />,
33
+ },
34
+ ];
35
+
36
+ export default function HazoNotifyLayout({
37
+ children,
38
+ }: {
39
+ children: React.ReactNode;
40
+ }) {
41
+ const pathname = usePathname();
42
+
43
+ return (
44
+ <div className="cls_hazo_notify_layout flex h-screen w-full overflow-hidden">
45
+ <Sidebar className="cls_hazo_notify_sidebar">
46
+ <SidebarHeader className="cls_hazo_notify_sidebar_header">
47
+ <h2 className="cls_hazo_notify_sidebar_title text-lg font-semibold">
48
+ Hazo Notify
49
+ </h2>
50
+ </SidebarHeader>
51
+ <SidebarContent className="cls_hazo_notify_sidebar_content">
52
+ <SidebarNav className="cls_hazo_notify_sidebar_nav">
53
+ {services.map((service) => (
54
+ <SidebarNavItem key={service.href} className="cls_hazo_notify_sidebar_nav_item">
55
+ <Link
56
+ href={service.href}
57
+ className={cn(
58
+ "flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
59
+ pathname === service.href
60
+ ? "bg-accent text-accent-foreground"
61
+ : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
62
+ )}
63
+ >
64
+ <span className="cls_hazo_notify_sidebar_nav_link_icon">{service.icon}</span>
65
+ <span className="cls_hazo_notify_sidebar_nav_link_text">{service.title}</span>
66
+ </Link>
67
+ </SidebarNavItem>
68
+ ))}
69
+ </SidebarNav>
70
+ </SidebarContent>
71
+ </Sidebar>
72
+ <main className="cls_hazo_notify_main flex-1 overflow-y-auto bg-background">
73
+ {children}
74
+ </main>
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Main hazo_notify page
3
+ * Shows a blank page under the sidebar layout
4
+ */
5
+
6
+ export default function HazoNotifyPage() {
7
+ return (
8
+ <div className="cls_hazo_notify_page h-full w-full">
9
+ {/* Blank page - content can be added here later */}
10
+ </div>
11
+ );
12
+ }
@@ -0,0 +1,26 @@
1
+ import type { Metadata } from 'next';
2
+ import { Inter } from 'next/font/google';
3
+ import './globals.css';
4
+ import { Toaster } from 'sonner';
5
+
6
+ const inter = Inter({ subsets: ['latin'] });
7
+
8
+ export const metadata: Metadata = {
9
+ title: 'Hazo Notify',
10
+ description: 'Email notification service library',
11
+ };
12
+
13
+ export default function RootLayout({
14
+ children,
15
+ }: {
16
+ children: React.ReactNode;
17
+ }) {
18
+ return (
19
+ <html lang="en">
20
+ <body className={inter.className}>
21
+ {children}
22
+ <Toaster />
23
+ </body>
24
+ </html>
25
+ );
26
+ }
@@ -0,0 +1,14 @@
1
+ export default function Home() {
2
+ return (
3
+ <div className="cls_home_container flex min-h-screen flex-col items-center justify-center p-24">
4
+ <div className="cls_home_content z-10 max-w-5xl w-full items-center justify-center font-mono text-sm">
5
+ <h1 className="cls_home_title text-4xl font-bold text-center mb-4">
6
+ Hazo Notify
7
+ </h1>
8
+ <p className="cls_home_description text-center text-muted-foreground">
9
+ Email notification service library
10
+ </p>
11
+ </div>
12
+ </div>
13
+ );
14
+ }
@@ -0,0 +1,61 @@
1
+ "use client"
2
+
3
+ import {
4
+ InitialConfigType,
5
+ LexicalComposer,
6
+ } from "@lexical/react/LexicalComposer"
7
+ import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin"
8
+ import { EditorState, SerializedEditorState } from "lexical"
9
+
10
+ import { editorTheme } from "@/components/editor/themes/editor-theme"
11
+ import { TooltipProvider } from "@/components/ui/tooltip"
12
+
13
+ import { nodes } from "./nodes"
14
+ import { Plugins } from "./plugins"
15
+
16
+ const editorConfig: InitialConfigType = {
17
+ namespace: "Editor",
18
+ theme: editorTheme,
19
+ nodes,
20
+ onError: (error: Error) => {
21
+ console.error(error)
22
+ },
23
+ }
24
+
25
+ export function Editor({
26
+ editorState,
27
+ editorSerializedState,
28
+ onChange,
29
+ onSerializedChange,
30
+ }: {
31
+ editorState?: EditorState
32
+ editorSerializedState?: SerializedEditorState
33
+ onChange?: (editorState: EditorState) => void
34
+ onSerializedChange?: (editorSerializedState: SerializedEditorState) => void
35
+ }) {
36
+ return (
37
+ <div className="bg-background overflow-hidden rounded-lg border shadow">
38
+ <LexicalComposer
39
+ initialConfig={{
40
+ ...editorConfig,
41
+ ...(editorState ? { editorState } : {}),
42
+ ...(editorSerializedState
43
+ ? { editorState: JSON.stringify(editorSerializedState) }
44
+ : {}),
45
+ }}
46
+ >
47
+ <TooltipProvider>
48
+ <Plugins />
49
+
50
+ <OnChangePlugin
51
+ ignoreSelectionChange={true}
52
+ onChange={(editorState) => {
53
+ onChange?.(editorState)
54
+ onSerializedChange?.(editorState.toJSON())
55
+ }}
56
+ />
57
+ </TooltipProvider>
58
+ </LexicalComposer>
59
+ </div>
60
+ )
61
+ }
@@ -0,0 +1,11 @@
1
+ import { HeadingNode, QuoteNode } from "@lexical/rich-text"
2
+ import {
3
+ Klass,
4
+ LexicalNode,
5
+ LexicalNodeReplacement,
6
+ ParagraphNode,
7
+ TextNode,
8
+ } from "lexical"
9
+
10
+ export const nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement> =
11
+ [HeadingNode, ParagraphNode, TextNode, QuoteNode]
@@ -0,0 +1,36 @@
1
+ import { useState } from "react"
2
+ import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"
3
+ import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"
4
+
5
+ import { ContentEditable } from "@/components/editor/editor-ui/content-editable"
6
+
7
+ export function Plugins() {
8
+ const [floatingAnchorElem, setFloatingAnchorElem] =
9
+ useState<HTMLDivElement | null>(null)
10
+
11
+ const onRef = (_floatingAnchorElem: HTMLDivElement) => {
12
+ if (_floatingAnchorElem !== null) {
13
+ setFloatingAnchorElem(_floatingAnchorElem)
14
+ }
15
+ }
16
+
17
+ return (
18
+ <div className="relative">
19
+ {/* toolbar plugins */}
20
+ <div className="relative">
21
+ <RichTextPlugin
22
+ contentEditable={
23
+ <div className="">
24
+ <div className="" ref={onRef}>
25
+ <ContentEditable placeholder={"Start typing ..."} />
26
+ </div>
27
+ </div>
28
+ }
29
+ ErrorBoundary={LexicalErrorBoundary}
30
+ />
31
+ {/* editor plugins */}
32
+ </div>
33
+ {/* actions plugins */}
34
+ </div>
35
+ )
36
+ }
@@ -0,0 +1,34 @@
1
+ import { JSX } from "react"
2
+ import { ContentEditable as LexicalContentEditable } from "@lexical/react/LexicalContentEditable"
3
+
4
+ type Props = {
5
+ placeholder: string
6
+ className?: string
7
+ placeholderClassName?: string
8
+ }
9
+
10
+ export function ContentEditable({
11
+ placeholder,
12
+ className,
13
+ placeholderClassName,
14
+ }: Props): JSX.Element {
15
+ return (
16
+ <LexicalContentEditable
17
+ className={
18
+ className ??
19
+ `ContentEditable__root relative block min-h-72 min-h-full overflow-auto px-8 py-4 focus:outline-none`
20
+ }
21
+ aria-placeholder={placeholder}
22
+ placeholder={
23
+ <div
24
+ className={
25
+ placeholderClassName ??
26
+ `text-muted-foreground pointer-events-none absolute top-0 left-0 overflow-hidden px-8 py-[18px] text-ellipsis select-none`
27
+ }
28
+ >
29
+ {placeholder}
30
+ </div>
31
+ }
32
+ />
33
+ )
34
+ }