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.
- package/.cursor/rules/db_schema.mdc +0 -0
- package/.cursor/rules/design.mdc +16 -0
- package/.cursor/rules/general.mdc +49 -0
- package/README.md +765 -0
- package/components/emailer-html-editor.tsx +94 -0
- package/components/ui/button.tsx +53 -0
- package/components/ui/card.tsx +78 -0
- package/components/ui/input.tsx +24 -0
- package/components/ui/label.tsx +21 -0
- package/components/ui/sidebar.tsx +121 -0
- package/components/ui/spinner.tsx +54 -0
- package/components/ui/textarea.tsx +23 -0
- package/components/ui/tooltip.tsx +30 -0
- package/components.json +20 -0
- package/hazo_notify_config.ini +153 -0
- package/jest.config.js +27 -0
- package/jest.setup.js +1 -0
- package/next.config.js +22 -0
- package/package.json +72 -0
- package/postcss.config.js +6 -0
- package/src/app/api/hazo_notify/emailer/send/__tests__/route.test.ts +227 -0
- package/src/app/api/hazo_notify/emailer/send/route.ts +537 -0
- package/src/app/editor-00/page.tsx +47 -0
- package/src/app/globals.css +69 -0
- package/src/app/hazo_notify/emailer_test/layout.tsx +53 -0
- package/src/app/hazo_notify/emailer_test/page.tsx +369 -0
- package/src/app/hazo_notify/layout.tsx +77 -0
- package/src/app/hazo_notify/page.tsx +12 -0
- package/src/app/layout.tsx +26 -0
- package/src/app/page.tsx +14 -0
- package/src/components/blocks/editor-00/editor.tsx +61 -0
- package/src/components/blocks/editor-00/nodes.ts +11 -0
- package/src/components/blocks/editor-00/plugins.tsx +36 -0
- package/src/components/editor/editor-ui/content-editable.tsx +34 -0
- package/src/components/editor/themes/editor-theme.css +91 -0
- package/src/components/editor/themes/editor-theme.ts +130 -0
- package/src/components/ui/button.tsx +53 -0
- package/src/components/ui/card.tsx +78 -0
- package/src/components/ui/input.tsx +24 -0
- package/src/components/ui/label.tsx +21 -0
- package/src/components/ui/sidebar.tsx +121 -0
- package/src/components/ui/spinner.tsx +54 -0
- package/src/components/ui/textarea.tsx +23 -0
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/lib/emailer/__tests__/emailer.test.ts +200 -0
- package/src/lib/emailer/emailer.ts +263 -0
- package/src/lib/emailer/index.ts +11 -0
- package/src/lib/emailer/providers/__tests__/zeptomail_provider.test.ts +196 -0
- package/src/lib/emailer/providers/index.ts +33 -0
- package/src/lib/emailer/providers/pop3_provider.ts +30 -0
- package/src/lib/emailer/providers/smtp_provider.ts +30 -0
- package/src/lib/emailer/providers/zeptomail_provider.ts +299 -0
- package/src/lib/emailer/types.ts +119 -0
- package/src/lib/emailer/utils/constants.ts +24 -0
- package/src/lib/emailer/utils/index.ts +9 -0
- package/src/lib/emailer/utils/logger.ts +71 -0
- package/src/lib/emailer/utils/validation.ts +84 -0
- package/src/lib/index.ts +6 -0
- package/src/lib/utils.ts +6 -0
- package/tailwind.config.ts +65 -0
- 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
|
+
}
|
package/src/app/page.tsx
ADDED
|
@@ -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
|
+
}
|