@volchoklv/newsletter-kit 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/dist/index.js ADDED
@@ -0,0 +1,1762 @@
1
+ "use client";
2
+
3
+ // src/utils/crypto.ts
4
+ import { randomBytes } from "crypto";
5
+ function generateToken(length = 32) {
6
+ return randomBytes(length).toString("hex");
7
+ }
8
+ function generateUrlSafeToken(length = 32) {
9
+ return randomBytes(length).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
10
+ }
11
+
12
+ // src/utils/validation.ts
13
+ function isValidEmail(email) {
14
+ if (!email || typeof email !== "string") return false;
15
+ const trimmed = email.trim();
16
+ if (trimmed.length === 0 || trimmed.length > 254) return false;
17
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
18
+ if (!emailRegex.test(trimmed)) return false;
19
+ const [localPart, domain] = trimmed.split("@");
20
+ if (!localPart || localPart.length > 64) return false;
21
+ if (localPart.startsWith(".") || localPart.endsWith(".")) return false;
22
+ if (localPart.includes("..")) return false;
23
+ if (!domain || domain.length > 253) return false;
24
+ if (domain.startsWith(".") || domain.endsWith(".")) return false;
25
+ if (domain.startsWith("-") || domain.endsWith("-")) return false;
26
+ if (!domain.includes(".")) return false;
27
+ const tld = domain.split(".").pop();
28
+ if (!tld || tld.length < 2) return false;
29
+ return true;
30
+ }
31
+ function normalizeEmail(email, options = {}) {
32
+ let normalized = email.toLowerCase().trim();
33
+ if (options.normalizeGmail) {
34
+ const [localPart, domain] = normalized.split("@");
35
+ if (domain === "gmail.com" || domain === "googlemail.com") {
36
+ const cleanLocal = localPart.split("+")[0].replace(/\./g, "");
37
+ normalized = `${cleanLocal}@gmail.com`;
38
+ }
39
+ }
40
+ return normalized;
41
+ }
42
+ var DISPOSABLE_DOMAINS = /* @__PURE__ */ new Set([
43
+ "tempmail.com",
44
+ "throwaway.email",
45
+ "guerrillamail.com",
46
+ "sharklasers.com",
47
+ "mailinator.com",
48
+ "yopmail.com",
49
+ "10minutemail.com",
50
+ "temp-mail.org",
51
+ "fakeinbox.com",
52
+ "trashmail.com",
53
+ "getnada.com",
54
+ "maildrop.cc",
55
+ "dispostable.com",
56
+ "tempail.com",
57
+ "mohmal.com"
58
+ ]);
59
+ function isDisposableEmail(email) {
60
+ const domain = email.toLowerCase().split("@")[1];
61
+ return DISPOSABLE_DOMAINS.has(domain);
62
+ }
63
+ function sanitizeString(input) {
64
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
65
+ }
66
+
67
+ // src/utils/rate-limit.ts
68
+ function createRateLimiter(config) {
69
+ const { max, windowSeconds, identifier } = config;
70
+ const store = /* @__PURE__ */ new Map();
71
+ setInterval(() => {
72
+ const now = Date.now();
73
+ for (const [key, entry] of store.entries()) {
74
+ if (entry.resetAt < now) {
75
+ store.delete(key);
76
+ }
77
+ }
78
+ }, windowSeconds * 1e3);
79
+ return {
80
+ /**
81
+ * Check if request should be rate limited
82
+ * Returns true if request is allowed, false if rate limited
83
+ */
84
+ async check(req) {
85
+ const id = identifier ? identifier(req) : getClientIP(req);
86
+ const now = Date.now();
87
+ const windowMs = windowSeconds * 1e3;
88
+ let entry = store.get(id);
89
+ if (!entry || entry.resetAt < now) {
90
+ entry = {
91
+ count: 0,
92
+ resetAt: now + windowMs
93
+ };
94
+ }
95
+ entry.count++;
96
+ store.set(id, entry);
97
+ const allowed = entry.count <= max;
98
+ const remaining = Math.max(0, max - entry.count);
99
+ return {
100
+ allowed,
101
+ remaining,
102
+ resetAt: new Date(entry.resetAt)
103
+ };
104
+ },
105
+ /**
106
+ * Get current limit status without incrementing
107
+ */
108
+ async status(req) {
109
+ const id = identifier ? identifier(req) : getClientIP(req);
110
+ const entry = store.get(id);
111
+ if (!entry || entry.resetAt < Date.now()) {
112
+ return { remaining: max, resetAt: null };
113
+ }
114
+ return {
115
+ remaining: Math.max(0, max - entry.count),
116
+ resetAt: new Date(entry.resetAt)
117
+ };
118
+ },
119
+ /**
120
+ * Reset rate limit for an identifier
121
+ */
122
+ async reset(req) {
123
+ const id = identifier ? identifier(req) : getClientIP(req);
124
+ store.delete(id);
125
+ }
126
+ };
127
+ }
128
+ function getClientIP(req) {
129
+ const headers = req.headers;
130
+ const cfConnectingIP = headers.get("cf-connecting-ip");
131
+ if (cfConnectingIP) return cfConnectingIP;
132
+ const xForwardedFor = headers.get("x-forwarded-for");
133
+ if (xForwardedFor) {
134
+ return xForwardedFor.split(",")[0].trim();
135
+ }
136
+ const xRealIP = headers.get("x-real-ip");
137
+ if (xRealIP) return xRealIP;
138
+ const xVercelForwardedFor = headers.get("x-vercel-forwarded-for");
139
+ if (xVercelForwardedFor) return xVercelForwardedFor.split(",")[0].trim();
140
+ return "unknown";
141
+ }
142
+ function createRateLimitHeaders(limit, remaining, resetAt) {
143
+ const headers = new Headers();
144
+ headers.set("X-RateLimit-Limit", limit.toString());
145
+ headers.set("X-RateLimit-Remaining", remaining.toString());
146
+ headers.set("X-RateLimit-Reset", Math.floor(resetAt.getTime() / 1e3).toString());
147
+ return headers;
148
+ }
149
+ var defaultRateLimitConfig = {
150
+ max: 5,
151
+ windowSeconds: 60
152
+ // 5 requests per minute
153
+ };
154
+
155
+ // src/adapters/storage/memory.ts
156
+ function createMemoryAdapter() {
157
+ const subscribers = /* @__PURE__ */ new Map();
158
+ const tokenIndex = /* @__PURE__ */ new Map();
159
+ return {
160
+ async createSubscriber(input, token) {
161
+ const email = input.email.toLowerCase();
162
+ const confirmToken = token || generateToken();
163
+ const now = /* @__PURE__ */ new Date();
164
+ const existing = subscribers.get(email);
165
+ if (existing?.id) {
166
+ for (const [t, e] of tokenIndex.entries()) {
167
+ if (e === email) {
168
+ tokenIndex.delete(t);
169
+ break;
170
+ }
171
+ }
172
+ }
173
+ const subscriber = {
174
+ id: existing?.id || crypto.randomUUID(),
175
+ email,
176
+ status: "pending",
177
+ source: input.source,
178
+ tags: input.tags || [],
179
+ metadata: input.metadata,
180
+ consentIp: input.ip,
181
+ consentAt: now,
182
+ confirmedAt: void 0,
183
+ unsubscribedAt: void 0,
184
+ createdAt: existing?.createdAt || now,
185
+ updatedAt: now
186
+ };
187
+ subscribers.set(email, subscriber);
188
+ tokenIndex.set(confirmToken, email);
189
+ return { ...subscriber, id: confirmToken };
190
+ },
191
+ async getSubscriberByEmail(email) {
192
+ return subscribers.get(email.toLowerCase()) || null;
193
+ },
194
+ async getSubscriberByToken(token) {
195
+ const email = tokenIndex.get(token);
196
+ if (!email) return null;
197
+ return subscribers.get(email) || null;
198
+ },
199
+ async confirmSubscriber(token) {
200
+ const email = tokenIndex.get(token);
201
+ if (!email) return null;
202
+ const subscriber = subscribers.get(email);
203
+ if (!subscriber) return null;
204
+ const updated = {
205
+ ...subscriber,
206
+ status: "confirmed",
207
+ confirmedAt: /* @__PURE__ */ new Date(),
208
+ updatedAt: /* @__PURE__ */ new Date()
209
+ };
210
+ subscribers.set(email, updated);
211
+ tokenIndex.delete(token);
212
+ return updated;
213
+ },
214
+ async unsubscribe(email) {
215
+ const subscriber = subscribers.get(email.toLowerCase());
216
+ if (!subscriber) return false;
217
+ const updated = {
218
+ ...subscriber,
219
+ status: "unsubscribed",
220
+ unsubscribedAt: /* @__PURE__ */ new Date(),
221
+ updatedAt: /* @__PURE__ */ new Date()
222
+ };
223
+ subscribers.set(email.toLowerCase(), updated);
224
+ return true;
225
+ },
226
+ async listSubscribers(options) {
227
+ let results = Array.from(subscribers.values());
228
+ if (options?.status) {
229
+ results = results.filter((s) => s.status === options.status);
230
+ }
231
+ if (options?.source) {
232
+ results = results.filter((s) => s.source === options.source);
233
+ }
234
+ if (options?.tags?.length) {
235
+ results = results.filter(
236
+ (s) => options.tags.some((tag) => s.tags?.includes(tag))
237
+ );
238
+ }
239
+ results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
240
+ const total = results.length;
241
+ const offset = options?.offset || 0;
242
+ const limit = options?.limit || 100;
243
+ return {
244
+ subscribers: results.slice(offset, offset + limit),
245
+ total
246
+ };
247
+ },
248
+ async deleteSubscriber(email) {
249
+ const normalizedEmail = email.toLowerCase();
250
+ const existed = subscribers.has(normalizedEmail);
251
+ subscribers.delete(normalizedEmail);
252
+ for (const [token, e] of tokenIndex.entries()) {
253
+ if (e === normalizedEmail) {
254
+ tokenIndex.delete(token);
255
+ break;
256
+ }
257
+ }
258
+ return existed;
259
+ },
260
+ async updateSubscriber(email, data) {
261
+ const normalizedEmail = email.toLowerCase();
262
+ const subscriber = subscribers.get(normalizedEmail);
263
+ if (!subscriber) return null;
264
+ const updated = {
265
+ ...subscriber,
266
+ ...data.source !== void 0 && { source: data.source },
267
+ ...data.tags !== void 0 && { tags: data.tags },
268
+ ...data.metadata !== void 0 && { metadata: data.metadata },
269
+ updatedAt: /* @__PURE__ */ new Date()
270
+ };
271
+ subscribers.set(normalizedEmail, updated);
272
+ return updated;
273
+ }
274
+ };
275
+ }
276
+ function createNoopAdapter() {
277
+ return {
278
+ async createSubscriber(input) {
279
+ return {
280
+ id: "noop",
281
+ email: input.email,
282
+ status: "pending",
283
+ source: input.source,
284
+ tags: input.tags,
285
+ metadata: input.metadata,
286
+ consentIp: input.ip,
287
+ consentAt: /* @__PURE__ */ new Date(),
288
+ createdAt: /* @__PURE__ */ new Date(),
289
+ updatedAt: /* @__PURE__ */ new Date()
290
+ };
291
+ },
292
+ async getSubscriberByEmail() {
293
+ return null;
294
+ },
295
+ async getSubscriberByToken() {
296
+ return null;
297
+ },
298
+ async confirmSubscriber() {
299
+ return null;
300
+ },
301
+ async unsubscribe() {
302
+ return true;
303
+ }
304
+ };
305
+ }
306
+
307
+ // src/server/handlers.ts
308
+ function createNewsletterHandlers(config) {
309
+ const {
310
+ emailAdapter,
311
+ storageAdapter = createMemoryAdapter(),
312
+ doubleOptIn = true,
313
+ baseUrl,
314
+ confirmPath = "/api/newsletter/confirm",
315
+ unsubscribePath = "/api/newsletter/unsubscribe",
316
+ honeypotField = "website",
317
+ rateLimit = defaultRateLimitConfig,
318
+ validateEmail: customValidateEmail,
319
+ allowedSources,
320
+ defaultTags = [],
321
+ onSubscribe,
322
+ onConfirm,
323
+ onUnsubscribe,
324
+ onError
325
+ } = config;
326
+ const rateLimiter = rateLimit ? createRateLimiter(rateLimit) : null;
327
+ function jsonResponse(data, status = 200, headers) {
328
+ const responseHeaders = new Headers(headers);
329
+ responseHeaders.set("Content-Type", "application/json");
330
+ return new Response(JSON.stringify(data), {
331
+ status,
332
+ headers: responseHeaders
333
+ });
334
+ }
335
+ async function handleSubscribe(req) {
336
+ const body = await req.json();
337
+ const { email, source, tags = [], metadata } = body;
338
+ if (honeypotField && body[honeypotField]) {
339
+ return {
340
+ success: true,
341
+ message: doubleOptIn ? "Please check your email to confirm your subscription." : "You have been subscribed!"
342
+ };
343
+ }
344
+ if (!email || !isValidEmail(email)) {
345
+ return {
346
+ success: false,
347
+ message: "Please enter a valid email address."
348
+ };
349
+ }
350
+ if (customValidateEmail) {
351
+ const isValid = await customValidateEmail(email);
352
+ if (!isValid) {
353
+ return {
354
+ success: false,
355
+ message: "This email address is not allowed."
356
+ };
357
+ }
358
+ }
359
+ if (source && allowedSources && !allowedSources.includes(source)) {
360
+ return {
361
+ success: false,
362
+ message: "Invalid subscription source."
363
+ };
364
+ }
365
+ const normalizedEmail = normalizeEmail(email);
366
+ const ip = getClientIP(req);
367
+ try {
368
+ const existing = await storageAdapter.getSubscriberByEmail(normalizedEmail);
369
+ if (existing?.status === "confirmed") {
370
+ return {
371
+ success: true,
372
+ message: "You are already subscribed!",
373
+ subscriber: existing
374
+ };
375
+ }
376
+ const token = generateToken();
377
+ const subscriber = await storageAdapter.createSubscriber(
378
+ {
379
+ email: normalizedEmail,
380
+ source,
381
+ tags: [...defaultTags, ...tags],
382
+ metadata,
383
+ ip
384
+ },
385
+ token
386
+ );
387
+ if (doubleOptIn) {
388
+ const confirmUrl = `${baseUrl}${confirmPath}?token=${token}`;
389
+ await emailAdapter.sendConfirmation(normalizedEmail, token, confirmUrl);
390
+ if (emailAdapter.notifyAdmin) {
391
+ await emailAdapter.notifyAdmin(subscriber);
392
+ }
393
+ if (onSubscribe) {
394
+ await onSubscribe(subscriber);
395
+ }
396
+ return {
397
+ success: true,
398
+ message: "Please check your email to confirm your subscription.",
399
+ subscriber,
400
+ requiresConfirmation: true
401
+ };
402
+ } else {
403
+ const confirmed = await storageAdapter.confirmSubscriber(token);
404
+ await emailAdapter.sendWelcome(normalizedEmail);
405
+ if (emailAdapter.notifyAdmin && confirmed) {
406
+ await emailAdapter.notifyAdmin(confirmed);
407
+ }
408
+ if (onSubscribe && confirmed) {
409
+ await onSubscribe(confirmed);
410
+ }
411
+ return {
412
+ success: true,
413
+ message: "You have been subscribed!",
414
+ subscriber: confirmed || subscriber,
415
+ requiresConfirmation: false
416
+ };
417
+ }
418
+ } catch (error) {
419
+ if (onError) {
420
+ await onError(error, "subscribe");
421
+ }
422
+ console.error("[newsletter-kit] Subscribe error:", error);
423
+ return {
424
+ success: false,
425
+ message: "Something went wrong. Please try again."
426
+ };
427
+ }
428
+ }
429
+ async function handleConfirm(token) {
430
+ if (!token) {
431
+ return {
432
+ success: false,
433
+ message: "Invalid confirmation link."
434
+ };
435
+ }
436
+ try {
437
+ const subscriber = await storageAdapter.confirmSubscriber(token);
438
+ if (!subscriber) {
439
+ return {
440
+ success: false,
441
+ message: "This confirmation link is invalid or has expired."
442
+ };
443
+ }
444
+ await emailAdapter.sendWelcome(subscriber.email);
445
+ if (onConfirm) {
446
+ await onConfirm(subscriber);
447
+ }
448
+ return {
449
+ success: true,
450
+ message: "Your subscription has been confirmed!",
451
+ subscriber
452
+ };
453
+ } catch (error) {
454
+ if (onError) {
455
+ await onError(error, "confirm");
456
+ }
457
+ console.error("[newsletter-kit] Confirm error:", error);
458
+ return {
459
+ success: false,
460
+ message: "Something went wrong. Please try again."
461
+ };
462
+ }
463
+ }
464
+ async function handleUnsubscribe(email) {
465
+ if (!email || !isValidEmail(email)) {
466
+ return {
467
+ success: false,
468
+ message: "Invalid email address."
469
+ };
470
+ }
471
+ const normalizedEmail = normalizeEmail(email);
472
+ try {
473
+ const success = await storageAdapter.unsubscribe(normalizedEmail);
474
+ if (!success) {
475
+ return {
476
+ success: false,
477
+ message: "Email not found in our list."
478
+ };
479
+ }
480
+ if (emailAdapter.sendUnsubscribed) {
481
+ const resubscribeUrl = `${baseUrl}`;
482
+ await emailAdapter.sendUnsubscribed(normalizedEmail, resubscribeUrl);
483
+ }
484
+ if (onUnsubscribe) {
485
+ await onUnsubscribe(normalizedEmail);
486
+ }
487
+ return {
488
+ success: true,
489
+ message: "You have been unsubscribed."
490
+ };
491
+ } catch (error) {
492
+ if (onError) {
493
+ await onError(error, "unsubscribe");
494
+ }
495
+ console.error("[newsletter-kit] Unsubscribe error:", error);
496
+ return {
497
+ success: false,
498
+ message: "Something went wrong. Please try again."
499
+ };
500
+ }
501
+ }
502
+ return {
503
+ /**
504
+ * POST /api/newsletter/subscribe
505
+ */
506
+ subscribe: async (req) => {
507
+ if (rateLimiter) {
508
+ const { allowed, remaining, resetAt } = await rateLimiter.check(req);
509
+ const headers = createRateLimitHeaders(rateLimit.max, remaining, resetAt);
510
+ if (!allowed) {
511
+ return jsonResponse(
512
+ {
513
+ success: false,
514
+ message: "Too many requests. Please try again later."
515
+ },
516
+ 429,
517
+ headers
518
+ );
519
+ }
520
+ }
521
+ const result = await handleSubscribe(req);
522
+ return jsonResponse(result, result.success ? 200 : 400);
523
+ },
524
+ /**
525
+ * GET /api/newsletter/confirm?token=xxx
526
+ *
527
+ * Can also return a redirect for better UX
528
+ */
529
+ confirm: async (req) => {
530
+ const url = new URL(req.url);
531
+ const token = url.searchParams.get("token");
532
+ const result = await handleConfirm(token || "");
533
+ const acceptsJson = req.headers.get("accept")?.includes("application/json");
534
+ if (acceptsJson) {
535
+ return jsonResponse(result, result.success ? 200 : 400);
536
+ }
537
+ const redirectUrl = new URL(baseUrl);
538
+ redirectUrl.searchParams.set("confirmed", result.success ? "true" : "false");
539
+ if (!result.success) {
540
+ redirectUrl.searchParams.set("error", result.message);
541
+ }
542
+ return Response.redirect(redirectUrl.toString(), 302);
543
+ },
544
+ /**
545
+ * POST /api/newsletter/unsubscribe
546
+ * Also supports GET with email query param
547
+ */
548
+ unsubscribe: async (req) => {
549
+ let email;
550
+ if (req.method === "GET") {
551
+ const url = new URL(req.url);
552
+ email = url.searchParams.get("email") || "";
553
+ } else {
554
+ const body = await req.json();
555
+ email = body.email;
556
+ }
557
+ const result = await handleUnsubscribe(email);
558
+ return jsonResponse(result, result.success ? 200 : 400);
559
+ },
560
+ /**
561
+ * Internal handlers for custom implementations
562
+ */
563
+ handlers: {
564
+ subscribe: handleSubscribe,
565
+ confirm: handleConfirm,
566
+ unsubscribe: handleUnsubscribe
567
+ },
568
+ /**
569
+ * Access to storage adapter for admin features
570
+ */
571
+ storage: storageAdapter,
572
+ /**
573
+ * Get subscriber by email (for admin/API use)
574
+ */
575
+ getSubscriber: async (email) => {
576
+ return storageAdapter.getSubscriberByEmail(normalizeEmail(email));
577
+ },
578
+ /**
579
+ * List subscribers (for admin/export use)
580
+ */
581
+ listSubscribers: storageAdapter.listSubscribers
582
+ };
583
+ }
584
+
585
+ // src/components/newsletter-form.tsx
586
+ import { useState, useCallback } from "react";
587
+
588
+ // src/utils/cn.ts
589
+ function cn(...inputs) {
590
+ return inputs.filter(Boolean).join(" ");
591
+ }
592
+
593
+ // src/components/newsletter-form.tsx
594
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
595
+ function NewsletterForm({
596
+ endpoint = "/api/newsletter/subscribe",
597
+ source,
598
+ tags,
599
+ honeypotField = "website",
600
+ className,
601
+ formClassName,
602
+ inputClassName,
603
+ buttonClassName,
604
+ messageClassName,
605
+ placeholder = "Enter your email",
606
+ buttonText = "Subscribe",
607
+ loadingText = "Subscribing...",
608
+ successMessage,
609
+ errorMessage,
610
+ showMessage = true,
611
+ onSuccess,
612
+ onError,
613
+ onSubmit,
614
+ disabled = false,
615
+ metadata
616
+ }) {
617
+ const [state, setState] = useState({
618
+ status: "idle",
619
+ message: "",
620
+ email: ""
621
+ });
622
+ const handleSubmit = useCallback(
623
+ async (e) => {
624
+ e.preventDefault();
625
+ const formData = new FormData(e.currentTarget);
626
+ const email = formData.get("email");
627
+ const honeypot = formData.get(honeypotField);
628
+ if (honeypot) {
629
+ setState({
630
+ status: "success",
631
+ message: successMessage || "Thanks for subscribing!",
632
+ email: ""
633
+ });
634
+ return;
635
+ }
636
+ if (!email || !email.includes("@")) {
637
+ setState({
638
+ status: "error",
639
+ message: "Please enter a valid email address.",
640
+ email
641
+ });
642
+ onError?.("Please enter a valid email address.");
643
+ return;
644
+ }
645
+ setState({ status: "loading", message: "", email });
646
+ onSubmit?.(email);
647
+ try {
648
+ const response = await fetch(endpoint, {
649
+ method: "POST",
650
+ headers: { "Content-Type": "application/json" },
651
+ body: JSON.stringify({
652
+ email,
653
+ source,
654
+ tags,
655
+ metadata
656
+ })
657
+ });
658
+ const data = await response.json();
659
+ if (response.ok && data.success) {
660
+ const message = successMessage || data.message || "Thanks for subscribing!";
661
+ setState({
662
+ status: "success",
663
+ message,
664
+ email: ""
665
+ });
666
+ onSuccess?.(email, message);
667
+ } else {
668
+ const message = errorMessage || data.message || "Something went wrong. Please try again.";
669
+ setState({
670
+ status: "error",
671
+ message,
672
+ email
673
+ });
674
+ onError?.(message);
675
+ }
676
+ } catch (error) {
677
+ const message = errorMessage || "Network error. Please try again.";
678
+ setState({
679
+ status: "error",
680
+ message,
681
+ email
682
+ });
683
+ onError?.(message);
684
+ }
685
+ },
686
+ [endpoint, source, tags, metadata, honeypotField, successMessage, errorMessage, onSuccess, onError, onSubmit]
687
+ );
688
+ const isLoading = state.status === "loading";
689
+ const isDisabled = disabled || isLoading;
690
+ return /* @__PURE__ */ jsxs("div", { className: cn("w-full", className), children: [
691
+ /* @__PURE__ */ jsxs(
692
+ "form",
693
+ {
694
+ onSubmit: handleSubmit,
695
+ className: cn("flex flex-col sm:flex-row gap-2", formClassName),
696
+ children: [
697
+ /* @__PURE__ */ jsx(
698
+ "input",
699
+ {
700
+ type: "text",
701
+ name: honeypotField,
702
+ autoComplete: "off",
703
+ tabIndex: -1,
704
+ "aria-hidden": "true",
705
+ className: "absolute -left-[9999px] opacity-0 pointer-events-none"
706
+ }
707
+ ),
708
+ /* @__PURE__ */ jsx(
709
+ "input",
710
+ {
711
+ type: "email",
712
+ name: "email",
713
+ placeholder,
714
+ required: true,
715
+ disabled: isDisabled,
716
+ defaultValue: state.email,
717
+ "aria-label": "Email address",
718
+ "aria-describedby": state.message ? "newsletter-message" : void 0,
719
+ className: cn(
720
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
721
+ "ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium",
722
+ "placeholder:text-muted-foreground",
723
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
724
+ "disabled:cursor-not-allowed disabled:opacity-50",
725
+ inputClassName
726
+ )
727
+ }
728
+ ),
729
+ /* @__PURE__ */ jsx(
730
+ "button",
731
+ {
732
+ type: "submit",
733
+ disabled: isDisabled,
734
+ className: cn(
735
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium",
736
+ "ring-offset-background transition-colors",
737
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
738
+ "disabled:pointer-events-none disabled:opacity-50",
739
+ "bg-primary text-primary-foreground hover:bg-primary/90",
740
+ "h-10 px-4 py-2",
741
+ "min-w-[120px]",
742
+ buttonClassName
743
+ ),
744
+ children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [
745
+ /* @__PURE__ */ jsxs(
746
+ "svg",
747
+ {
748
+ className: "mr-2 h-4 w-4 animate-spin",
749
+ xmlns: "http://www.w3.org/2000/svg",
750
+ fill: "none",
751
+ viewBox: "0 0 24 24",
752
+ children: [
753
+ /* @__PURE__ */ jsx(
754
+ "circle",
755
+ {
756
+ className: "opacity-25",
757
+ cx: "12",
758
+ cy: "12",
759
+ r: "10",
760
+ stroke: "currentColor",
761
+ strokeWidth: "4"
762
+ }
763
+ ),
764
+ /* @__PURE__ */ jsx(
765
+ "path",
766
+ {
767
+ className: "opacity-75",
768
+ fill: "currentColor",
769
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
770
+ }
771
+ )
772
+ ]
773
+ }
774
+ ),
775
+ loadingText
776
+ ] }) : buttonText
777
+ }
778
+ )
779
+ ]
780
+ }
781
+ ),
782
+ showMessage && state.message && /* @__PURE__ */ jsx(
783
+ "p",
784
+ {
785
+ id: "newsletter-message",
786
+ role: state.status === "error" ? "alert" : "status",
787
+ className: cn(
788
+ "mt-2 text-sm",
789
+ state.status === "success" && "text-green-600 dark:text-green-400",
790
+ state.status === "error" && "text-red-600 dark:text-red-400",
791
+ messageClassName
792
+ ),
793
+ children: state.message
794
+ }
795
+ )
796
+ ] });
797
+ }
798
+
799
+ // src/components/newsletter-blocks.tsx
800
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
801
+ function NewsletterBlock({
802
+ title = "Subscribe to our newsletter",
803
+ description = "Get the latest updates delivered directly to your inbox.",
804
+ containerClassName,
805
+ titleClassName,
806
+ descriptionClassName,
807
+ ...formProps
808
+ }) {
809
+ return /* @__PURE__ */ jsx2(
810
+ "section",
811
+ {
812
+ className: cn(
813
+ "w-full py-12 md:py-16 lg:py-20",
814
+ "bg-muted/50",
815
+ containerClassName
816
+ ),
817
+ children: /* @__PURE__ */ jsx2("div", { className: "container mx-auto px-4 md:px-6", children: /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center space-y-4 text-center", children: [
818
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
819
+ /* @__PURE__ */ jsx2(
820
+ "h2",
821
+ {
822
+ className: cn(
823
+ "text-2xl font-bold tracking-tight sm:text-3xl md:text-4xl",
824
+ titleClassName
825
+ ),
826
+ children: title
827
+ }
828
+ ),
829
+ /* @__PURE__ */ jsx2(
830
+ "p",
831
+ {
832
+ className: cn(
833
+ "max-w-[600px] text-muted-foreground md:text-lg",
834
+ descriptionClassName
835
+ ),
836
+ children: description
837
+ }
838
+ )
839
+ ] }),
840
+ /* @__PURE__ */ jsx2("div", { className: "w-full max-w-md", children: /* @__PURE__ */ jsx2(NewsletterForm, { ...formProps }) })
841
+ ] }) })
842
+ }
843
+ );
844
+ }
845
+ function NewsletterCard({
846
+ title = "Newsletter",
847
+ description = "Subscribe to get updates.",
848
+ cardClassName,
849
+ titleClassName,
850
+ descriptionClassName,
851
+ ...formProps
852
+ }) {
853
+ return /* @__PURE__ */ jsx2(
854
+ "div",
855
+ {
856
+ className: cn(
857
+ "rounded-lg border bg-card text-card-foreground shadow-sm",
858
+ "p-6",
859
+ cardClassName
860
+ ),
861
+ children: /* @__PURE__ */ jsxs2("div", { className: "space-y-4", children: [
862
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-1.5", children: [
863
+ /* @__PURE__ */ jsx2(
864
+ "h3",
865
+ {
866
+ className: cn(
867
+ "text-lg font-semibold leading-none tracking-tight",
868
+ titleClassName
869
+ ),
870
+ children: title
871
+ }
872
+ ),
873
+ /* @__PURE__ */ jsx2(
874
+ "p",
875
+ {
876
+ className: cn(
877
+ "text-sm text-muted-foreground",
878
+ descriptionClassName
879
+ ),
880
+ children: description
881
+ }
882
+ )
883
+ ] }),
884
+ /* @__PURE__ */ jsx2(NewsletterForm, { ...formProps })
885
+ ] })
886
+ }
887
+ );
888
+ }
889
+ function NewsletterInline({
890
+ label,
891
+ labelClassName,
892
+ ...formProps
893
+ }) {
894
+ return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2", children: [
895
+ label && /* @__PURE__ */ jsx2(
896
+ "label",
897
+ {
898
+ className: cn(
899
+ "text-sm font-medium leading-none",
900
+ labelClassName
901
+ ),
902
+ children: label
903
+ }
904
+ ),
905
+ /* @__PURE__ */ jsx2(NewsletterForm, { ...formProps })
906
+ ] });
907
+ }
908
+ function NewsletterModalContent({
909
+ title = "Subscribe to our newsletter",
910
+ description = "We'll send you the best content, no spam.",
911
+ titleClassName,
912
+ descriptionClassName,
913
+ ...formProps
914
+ }) {
915
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-4", children: [
916
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-2 text-center", children: [
917
+ /* @__PURE__ */ jsx2(
918
+ "h2",
919
+ {
920
+ className: cn(
921
+ "text-lg font-semibold leading-none tracking-tight",
922
+ titleClassName
923
+ ),
924
+ children: title
925
+ }
926
+ ),
927
+ /* @__PURE__ */ jsx2(
928
+ "p",
929
+ {
930
+ className: cn(
931
+ "text-sm text-muted-foreground",
932
+ descriptionClassName
933
+ ),
934
+ children: description
935
+ }
936
+ )
937
+ ] }),
938
+ /* @__PURE__ */ jsx2(
939
+ NewsletterForm,
940
+ {
941
+ ...formProps,
942
+ formClassName: "flex-col",
943
+ buttonClassName: "w-full"
944
+ }
945
+ )
946
+ ] });
947
+ }
948
+ function NewsletterFooter({
949
+ title = "Newsletter",
950
+ description,
951
+ containerClassName,
952
+ titleClassName,
953
+ descriptionClassName,
954
+ privacyText,
955
+ privacyLink,
956
+ ...formProps
957
+ }) {
958
+ return /* @__PURE__ */ jsxs2("div", { className: cn("space-y-4", containerClassName), children: [
959
+ title && /* @__PURE__ */ jsx2(
960
+ "h3",
961
+ {
962
+ className: cn(
963
+ "text-sm font-semibold uppercase tracking-wider",
964
+ titleClassName
965
+ ),
966
+ children: title
967
+ }
968
+ ),
969
+ description && /* @__PURE__ */ jsx2(
970
+ "p",
971
+ {
972
+ className: cn(
973
+ "text-sm text-muted-foreground",
974
+ descriptionClassName
975
+ ),
976
+ children: description
977
+ }
978
+ ),
979
+ /* @__PURE__ */ jsx2(NewsletterForm, { ...formProps }),
980
+ privacyText && /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground", children: privacyLink ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
981
+ privacyText.replace(/\.$/, ""),
982
+ ".",
983
+ " ",
984
+ /* @__PURE__ */ jsx2("a", { href: privacyLink, className: "underline hover:text-foreground", children: "Privacy Policy" })
985
+ ] }) : privacyText })
986
+ ] });
987
+ }
988
+
989
+ // src/hooks/use-newsletter.ts
990
+ import { useState as useState2, useCallback as useCallback2 } from "react";
991
+ function useNewsletter(options = {}) {
992
+ const {
993
+ endpoint = "/api/newsletter/subscribe",
994
+ source,
995
+ tags,
996
+ metadata,
997
+ onSuccess,
998
+ onError
999
+ } = options;
1000
+ const [state, setState] = useState2({
1001
+ status: "idle",
1002
+ message: "",
1003
+ email: null
1004
+ });
1005
+ const subscribe = useCallback2(
1006
+ async (email) => {
1007
+ if (!email || !email.includes("@")) {
1008
+ const errorMsg = "Please enter a valid email address.";
1009
+ setState({
1010
+ status: "error",
1011
+ message: errorMsg,
1012
+ email
1013
+ });
1014
+ onError?.(errorMsg);
1015
+ return false;
1016
+ }
1017
+ setState({ status: "loading", message: "", email });
1018
+ try {
1019
+ const response = await fetch(endpoint, {
1020
+ method: "POST",
1021
+ headers: { "Content-Type": "application/json" },
1022
+ body: JSON.stringify({
1023
+ email,
1024
+ source,
1025
+ tags,
1026
+ metadata
1027
+ })
1028
+ });
1029
+ const data = await response.json();
1030
+ if (response.ok && data.success) {
1031
+ const message = data.message || "Thanks for subscribing!";
1032
+ setState({
1033
+ status: "success",
1034
+ message,
1035
+ email
1036
+ });
1037
+ onSuccess?.(email, message);
1038
+ return true;
1039
+ } else {
1040
+ const message = data.message || "Something went wrong. Please try again.";
1041
+ setState({
1042
+ status: "error",
1043
+ message,
1044
+ email
1045
+ });
1046
+ onError?.(message);
1047
+ return false;
1048
+ }
1049
+ } catch (error) {
1050
+ const message = "Network error. Please try again.";
1051
+ setState({
1052
+ status: "error",
1053
+ message,
1054
+ email
1055
+ });
1056
+ onError?.(message);
1057
+ return false;
1058
+ }
1059
+ },
1060
+ [endpoint, source, tags, metadata, onSuccess, onError]
1061
+ );
1062
+ const reset = useCallback2(() => {
1063
+ setState({
1064
+ status: "idle",
1065
+ message: "",
1066
+ email: null
1067
+ });
1068
+ }, []);
1069
+ return {
1070
+ ...state,
1071
+ subscribe,
1072
+ reset,
1073
+ isLoading: state.status === "loading",
1074
+ isSuccess: state.status === "success",
1075
+ isError: state.status === "error"
1076
+ };
1077
+ }
1078
+
1079
+ // src/adapters/email/resend.ts
1080
+ function createResendAdapter(config) {
1081
+ const { apiKey, from, replyTo, templates, adminEmail } = config;
1082
+ async function sendEmail(params) {
1083
+ const { Resend } = await import("resend");
1084
+ const resend = new Resend(apiKey);
1085
+ await resend.emails.send({
1086
+ from,
1087
+ replyTo,
1088
+ to: params.to,
1089
+ subject: params.subject,
1090
+ html: params.html,
1091
+ text: params.text
1092
+ });
1093
+ }
1094
+ const defaultTemplates = {
1095
+ confirmation: {
1096
+ subject: "Confirm your subscription",
1097
+ text: void 0,
1098
+ html: ({ confirmUrl, email }) => `
1099
+ <!DOCTYPE html>
1100
+ <html>
1101
+ <head>
1102
+ <meta charset="utf-8">
1103
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1104
+ </head>
1105
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
1106
+ <h1 style="color: #111; font-size: 24px; margin-bottom: 20px;">Confirm your subscription</h1>
1107
+ <p>Thanks for subscribing! Please confirm your email address by clicking the button below:</p>
1108
+ <a href="${confirmUrl}" style="display: inline-block; background-color: #000; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin: 20px 0;">
1109
+ Confirm Subscription
1110
+ </a>
1111
+ <p style="color: #666; font-size: 14px; margin-top: 30px;">
1112
+ If you didn't subscribe to this newsletter, you can safely ignore this email.
1113
+ </p>
1114
+ <p style="color: #999; font-size: 12px; margin-top: 20px;">
1115
+ This confirmation was requested for ${email}
1116
+ </p>
1117
+ </body>
1118
+ </html>
1119
+ `
1120
+ },
1121
+ welcome: {
1122
+ subject: "Welcome to our newsletter!",
1123
+ text: void 0,
1124
+ html: ({ email }) => `
1125
+ <!DOCTYPE html>
1126
+ <html>
1127
+ <head>
1128
+ <meta charset="utf-8">
1129
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1130
+ </head>
1131
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
1132
+ <h1 style="color: #111; font-size: 24px; margin-bottom: 20px;">You're subscribed! \u{1F389}</h1>
1133
+ <p>Thanks for confirming your subscription. You'll now receive our updates at <strong>${email}</strong>.</p>
1134
+ <p>We're excited to have you!</p>
1135
+ </body>
1136
+ </html>
1137
+ `
1138
+ },
1139
+ unsubscribed: {
1140
+ subject: "You have been unsubscribed",
1141
+ text: void 0,
1142
+ html: ({ email, resubscribeUrl }) => `
1143
+ <!DOCTYPE html>
1144
+ <html>
1145
+ <head>
1146
+ <meta charset="utf-8">
1147
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1148
+ </head>
1149
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
1150
+ <h1 style="color: #111; font-size: 24px; margin-bottom: 20px;">Unsubscribed</h1>
1151
+ <p>You have been unsubscribed from our newsletter. We're sorry to see you go!</p>
1152
+ ${resubscribeUrl ? `<p><a href="${resubscribeUrl}">Changed your mind? Resubscribe here.</a></p>` : ""}
1153
+ <p style="color: #999; font-size: 12px; margin-top: 20px;">
1154
+ This email was sent to ${email}
1155
+ </p>
1156
+ </body>
1157
+ </html>
1158
+ `
1159
+ }
1160
+ };
1161
+ return {
1162
+ async sendConfirmation(email, token, confirmUrl) {
1163
+ const template = templates?.confirmation ?? defaultTemplates.confirmation;
1164
+ const subject = template.subject ?? defaultTemplates.confirmation.subject;
1165
+ const html = template.html ?? defaultTemplates.confirmation.html;
1166
+ await sendEmail({
1167
+ to: email,
1168
+ subject,
1169
+ html: html({ confirmUrl, email }),
1170
+ text: template.text?.({ confirmUrl, email })
1171
+ });
1172
+ },
1173
+ async sendWelcome(email) {
1174
+ const template = templates?.welcome ?? defaultTemplates.welcome;
1175
+ const subject = template.subject ?? defaultTemplates.welcome.subject;
1176
+ const html = template.html ?? defaultTemplates.welcome.html;
1177
+ await sendEmail({
1178
+ to: email,
1179
+ subject,
1180
+ html: html({ email }),
1181
+ text: template.text?.({ email })
1182
+ });
1183
+ },
1184
+ async sendUnsubscribed(email, resubscribeUrl) {
1185
+ const template = templates?.unsubscribed ?? defaultTemplates.unsubscribed;
1186
+ const subject = template.subject ?? defaultTemplates.unsubscribed.subject;
1187
+ const html = template.html ?? defaultTemplates.unsubscribed.html;
1188
+ await sendEmail({
1189
+ to: email,
1190
+ subject,
1191
+ html: html({ email, resubscribeUrl }),
1192
+ text: template.text?.({ email, resubscribeUrl })
1193
+ });
1194
+ },
1195
+ async notifyAdmin(subscriber) {
1196
+ if (!adminEmail) return;
1197
+ await sendEmail({
1198
+ to: adminEmail,
1199
+ subject: `New newsletter subscriber: ${subscriber.email}`,
1200
+ html: `
1201
+ <!DOCTYPE html>
1202
+ <html>
1203
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; padding: 20px;">
1204
+ <h2>New Newsletter Subscriber</h2>
1205
+ <p><strong>Email:</strong> ${subscriber.email}</p>
1206
+ <p><strong>Status:</strong> ${subscriber.status}</p>
1207
+ ${subscriber.source ? `<p><strong>Source:</strong> ${subscriber.source}</p>` : ""}
1208
+ ${subscriber.tags?.length ? `<p><strong>Tags:</strong> ${subscriber.tags.join(", ")}</p>` : ""}
1209
+ <p><strong>Subscribed at:</strong> ${subscriber.createdAt.toISOString()}</p>
1210
+ </body>
1211
+ </html>
1212
+ `
1213
+ });
1214
+ }
1215
+ };
1216
+ }
1217
+
1218
+ // src/adapters/email/nodemailer.ts
1219
+ function createNodemailerAdapter(config) {
1220
+ const { smtp, from, replyTo, templates, adminEmail } = config;
1221
+ async function sendEmail(params) {
1222
+ const nodemailer = await import("nodemailer");
1223
+ const transporter = nodemailer.createTransport(smtp);
1224
+ await transporter.sendMail({
1225
+ from,
1226
+ replyTo,
1227
+ to: params.to,
1228
+ subject: params.subject,
1229
+ html: params.html,
1230
+ text: params.text
1231
+ });
1232
+ }
1233
+ const defaultTemplates = {
1234
+ confirmation: {
1235
+ subject: "Confirm your subscription",
1236
+ text: void 0,
1237
+ html: ({ confirmUrl, email }) => `
1238
+ <!DOCTYPE html>
1239
+ <html>
1240
+ <head>
1241
+ <meta charset="utf-8">
1242
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1243
+ </head>
1244
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
1245
+ <h1 style="color: #111; font-size: 24px; margin-bottom: 20px;">Confirm your subscription</h1>
1246
+ <p>Thanks for subscribing! Please confirm your email address by clicking the button below:</p>
1247
+ <a href="${confirmUrl}" style="display: inline-block; background-color: #000; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin: 20px 0;">
1248
+ Confirm Subscription
1249
+ </a>
1250
+ <p style="color: #666; font-size: 14px; margin-top: 30px;">
1251
+ If you didn't subscribe to this newsletter, you can safely ignore this email.
1252
+ </p>
1253
+ <p style="color: #999; font-size: 12px; margin-top: 20px;">
1254
+ This confirmation was requested for ${email}
1255
+ </p>
1256
+ </body>
1257
+ </html>
1258
+ `
1259
+ },
1260
+ welcome: {
1261
+ subject: "Welcome to our newsletter!",
1262
+ text: void 0,
1263
+ html: ({ email }) => `
1264
+ <!DOCTYPE html>
1265
+ <html>
1266
+ <head>
1267
+ <meta charset="utf-8">
1268
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1269
+ </head>
1270
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
1271
+ <h1 style="color: #111; font-size: 24px; margin-bottom: 20px;">You're subscribed! \u{1F389}</h1>
1272
+ <p>Thanks for confirming your subscription. You'll now receive our updates at <strong>${email}</strong>.</p>
1273
+ <p>We're excited to have you!</p>
1274
+ </body>
1275
+ </html>
1276
+ `
1277
+ },
1278
+ unsubscribed: {
1279
+ subject: "You have been unsubscribed",
1280
+ text: void 0,
1281
+ html: ({ email, resubscribeUrl }) => `
1282
+ <!DOCTYPE html>
1283
+ <html>
1284
+ <head>
1285
+ <meta charset="utf-8">
1286
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1287
+ </head>
1288
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
1289
+ <h1 style="color: #111; font-size: 24px; margin-bottom: 20px;">Unsubscribed</h1>
1290
+ <p>You have been unsubscribed from our newsletter. We're sorry to see you go!</p>
1291
+ ${resubscribeUrl ? `<p><a href="${resubscribeUrl}">Changed your mind? Resubscribe here.</a></p>` : ""}
1292
+ <p style="color: #999; font-size: 12px; margin-top: 20px;">
1293
+ This email was sent to ${email}
1294
+ </p>
1295
+ </body>
1296
+ </html>
1297
+ `
1298
+ }
1299
+ };
1300
+ return {
1301
+ async sendConfirmation(email, token, confirmUrl) {
1302
+ const template = templates?.confirmation ?? defaultTemplates.confirmation;
1303
+ const subject = template.subject ?? defaultTemplates.confirmation.subject;
1304
+ const html = template.html ?? defaultTemplates.confirmation.html;
1305
+ await sendEmail({
1306
+ to: email,
1307
+ subject,
1308
+ html: html({ confirmUrl, email }),
1309
+ text: template.text?.({ confirmUrl, email })
1310
+ });
1311
+ },
1312
+ async sendWelcome(email) {
1313
+ const template = templates?.welcome ?? defaultTemplates.welcome;
1314
+ const subject = template.subject ?? defaultTemplates.welcome.subject;
1315
+ const html = template.html ?? defaultTemplates.welcome.html;
1316
+ await sendEmail({
1317
+ to: email,
1318
+ subject,
1319
+ html: html({ email }),
1320
+ text: template.text?.({ email })
1321
+ });
1322
+ },
1323
+ async sendUnsubscribed(email, resubscribeUrl) {
1324
+ const template = templates?.unsubscribed ?? defaultTemplates.unsubscribed;
1325
+ const subject = template.subject ?? defaultTemplates.unsubscribed.subject;
1326
+ const html = template.html ?? defaultTemplates.unsubscribed.html;
1327
+ await sendEmail({
1328
+ to: email,
1329
+ subject,
1330
+ html: html({ email, resubscribeUrl }),
1331
+ text: template.text?.({ email, resubscribeUrl })
1332
+ });
1333
+ },
1334
+ async notifyAdmin(subscriber) {
1335
+ if (!adminEmail) return;
1336
+ await sendEmail({
1337
+ to: adminEmail,
1338
+ subject: `New newsletter subscriber: ${subscriber.email}`,
1339
+ html: `
1340
+ <!DOCTYPE html>
1341
+ <html>
1342
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; padding: 20px;">
1343
+ <h2>New Newsletter Subscriber</h2>
1344
+ <p><strong>Email:</strong> ${subscriber.email}</p>
1345
+ <p><strong>Status:</strong> ${subscriber.status}</p>
1346
+ ${subscriber.source ? `<p><strong>Source:</strong> ${subscriber.source}</p>` : ""}
1347
+ ${subscriber.tags?.length ? `<p><strong>Tags:</strong> ${subscriber.tags.join(", ")}</p>` : ""}
1348
+ <p><strong>Subscribed at:</strong> ${subscriber.createdAt.toISOString()}</p>
1349
+ </body>
1350
+ </html>
1351
+ `
1352
+ });
1353
+ }
1354
+ };
1355
+ }
1356
+
1357
+ // src/adapters/email/mailchimp.ts
1358
+ function createMailchimpAdapter(config) {
1359
+ const { apiKey, server, listId, from, adminEmail, useAsStorage } = config;
1360
+ async function getClient() {
1361
+ const mailchimp = await import("@mailchimp/mailchimp_marketing");
1362
+ mailchimp.default.setConfig({
1363
+ apiKey,
1364
+ server
1365
+ });
1366
+ return mailchimp.default;
1367
+ }
1368
+ async function addOrUpdateMember(email, status) {
1369
+ const client = await getClient();
1370
+ const subscriberHash = await hashEmail(email);
1371
+ try {
1372
+ await client.lists.setListMember(listId, subscriberHash, {
1373
+ email_address: email,
1374
+ status_if_new: status
1375
+ });
1376
+ } catch (error) {
1377
+ await client.lists.addListMember(listId, {
1378
+ email_address: email,
1379
+ status
1380
+ });
1381
+ }
1382
+ }
1383
+ async function hashEmail(email) {
1384
+ const crypto2 = await import("crypto");
1385
+ return crypto2.createHash("md5").update(email.toLowerCase()).digest("hex");
1386
+ }
1387
+ return {
1388
+ async sendConfirmation(email, _token, _confirmUrl) {
1389
+ if (useAsStorage) {
1390
+ await addOrUpdateMember(email, "pending");
1391
+ }
1392
+ },
1393
+ async sendWelcome(email) {
1394
+ if (useAsStorage) {
1395
+ await addOrUpdateMember(email, "subscribed");
1396
+ }
1397
+ },
1398
+ async sendUnsubscribed(email) {
1399
+ if (useAsStorage) {
1400
+ const client = await getClient();
1401
+ const subscriberHash = await hashEmail(email);
1402
+ await client.lists.updateListMember(listId, subscriberHash, {
1403
+ status: "unsubscribed"
1404
+ });
1405
+ }
1406
+ },
1407
+ async notifyAdmin(subscriber) {
1408
+ if (!adminEmail) return;
1409
+ console.warn(
1410
+ `[newsletter-kit] Mailchimp adapter: Admin notification not implemented. Consider setting up a Mailchimp webhook or automation. New subscriber: ${subscriber.email}`
1411
+ );
1412
+ }
1413
+ };
1414
+ }
1415
+ function createMailchimpStorageAdapter(config) {
1416
+ const { apiKey, server, listId } = config;
1417
+ async function getClient() {
1418
+ const mailchimp = await import("@mailchimp/mailchimp_marketing");
1419
+ mailchimp.default.setConfig({
1420
+ apiKey,
1421
+ server
1422
+ });
1423
+ return mailchimp.default;
1424
+ }
1425
+ async function hashEmail(email) {
1426
+ const crypto2 = await import("crypto");
1427
+ return crypto2.createHash("md5").update(email.toLowerCase()).digest("hex");
1428
+ }
1429
+ return {
1430
+ async createSubscriber(input) {
1431
+ const client = await getClient();
1432
+ await client.lists.addListMember(listId, {
1433
+ email_address: input.email,
1434
+ status: "pending",
1435
+ // Mailchimp handles double opt-in
1436
+ tags: input.tags,
1437
+ merge_fields: {
1438
+ SOURCE: input.source || ""
1439
+ }
1440
+ });
1441
+ return {
1442
+ id: await hashEmail(input.email),
1443
+ email: input.email,
1444
+ status: "pending",
1445
+ source: input.source,
1446
+ tags: input.tags,
1447
+ createdAt: /* @__PURE__ */ new Date(),
1448
+ updatedAt: /* @__PURE__ */ new Date()
1449
+ };
1450
+ },
1451
+ async getSubscriberByEmail(email) {
1452
+ const client = await getClient();
1453
+ const hash = await hashEmail(email);
1454
+ try {
1455
+ const member = await client.lists.getListMember(listId, hash);
1456
+ return {
1457
+ id: hash,
1458
+ email: member.email_address,
1459
+ status: member.status === "subscribed" ? "confirmed" : member.status,
1460
+ tags: member.tags?.map((t) => t.name),
1461
+ createdAt: new Date(member.timestamp_signup || member.timestamp_opt || Date.now()),
1462
+ updatedAt: new Date(member.last_changed || Date.now())
1463
+ };
1464
+ } catch {
1465
+ return null;
1466
+ }
1467
+ },
1468
+ // Note: Mailchimp doesn't use tokens - it has its own confirmation system
1469
+ async getSubscriberByToken() {
1470
+ console.warn("[newsletter-kit] Mailchimp uses its own confirmation system");
1471
+ return null;
1472
+ },
1473
+ async confirmSubscriber(_token) {
1474
+ console.warn("[newsletter-kit] Mailchimp handles confirmation automatically");
1475
+ return null;
1476
+ },
1477
+ async unsubscribe(email) {
1478
+ const client = await getClient();
1479
+ const hash = await hashEmail(email);
1480
+ await client.lists.updateListMember(listId, hash, {
1481
+ status: "unsubscribed"
1482
+ });
1483
+ return true;
1484
+ }
1485
+ };
1486
+ }
1487
+
1488
+ // src/adapters/storage/prisma.ts
1489
+ function mapToSubscriber(record) {
1490
+ return {
1491
+ id: record.id,
1492
+ email: record.email,
1493
+ status: record.status,
1494
+ source: record.source,
1495
+ tags: record.tags,
1496
+ metadata: record.metadata,
1497
+ consentIp: record.consentIp,
1498
+ consentAt: record.consentAt ? new Date(record.consentAt) : void 0,
1499
+ confirmedAt: record.confirmedAt ? new Date(record.confirmedAt) : void 0,
1500
+ unsubscribedAt: record.unsubscribedAt ? new Date(record.unsubscribedAt) : void 0,
1501
+ createdAt: new Date(record.createdAt),
1502
+ updatedAt: new Date(record.updatedAt)
1503
+ };
1504
+ }
1505
+ function createPrismaAdapter(config) {
1506
+ const { prisma } = config;
1507
+ const model = prisma.newsletterSubscriber;
1508
+ return {
1509
+ async createSubscriber(input, token) {
1510
+ const confirmToken = token || generateToken();
1511
+ const record = await model.upsert({
1512
+ where: { email: input.email.toLowerCase() },
1513
+ update: {
1514
+ token: confirmToken,
1515
+ source: input.source,
1516
+ tags: input.tags || [],
1517
+ metadata: input.metadata,
1518
+ consentIp: input.ip,
1519
+ consentAt: /* @__PURE__ */ new Date(),
1520
+ // Reset to pending if resubscribing
1521
+ status: "pending",
1522
+ unsubscribedAt: null
1523
+ },
1524
+ create: {
1525
+ email: input.email.toLowerCase(),
1526
+ token: confirmToken,
1527
+ status: "pending",
1528
+ source: input.source,
1529
+ tags: input.tags || [],
1530
+ metadata: input.metadata,
1531
+ consentIp: input.ip,
1532
+ consentAt: /* @__PURE__ */ new Date()
1533
+ }
1534
+ });
1535
+ return mapToSubscriber(record);
1536
+ },
1537
+ async getSubscriberByEmail(email) {
1538
+ const record = await model.findUnique({
1539
+ where: { email: email.toLowerCase() }
1540
+ });
1541
+ return record ? mapToSubscriber(record) : null;
1542
+ },
1543
+ async getSubscriberByToken(token) {
1544
+ const record = await model.findFirst({
1545
+ where: { token }
1546
+ });
1547
+ return record ? mapToSubscriber(record) : null;
1548
+ },
1549
+ async confirmSubscriber(token) {
1550
+ try {
1551
+ const record = await model.update({
1552
+ where: { token },
1553
+ data: {
1554
+ status: "confirmed",
1555
+ confirmedAt: /* @__PURE__ */ new Date(),
1556
+ token: null
1557
+ // Clear token after use
1558
+ }
1559
+ });
1560
+ return mapToSubscriber(record);
1561
+ } catch {
1562
+ return null;
1563
+ }
1564
+ },
1565
+ async unsubscribe(email) {
1566
+ try {
1567
+ await model.update({
1568
+ where: { email: email.toLowerCase() },
1569
+ data: {
1570
+ status: "unsubscribed",
1571
+ unsubscribedAt: /* @__PURE__ */ new Date()
1572
+ }
1573
+ });
1574
+ return true;
1575
+ } catch {
1576
+ return false;
1577
+ }
1578
+ },
1579
+ async listSubscribers(options) {
1580
+ const where = {};
1581
+ if (options?.status) {
1582
+ where.status = options.status;
1583
+ }
1584
+ if (options?.source) {
1585
+ where.source = options.source;
1586
+ }
1587
+ if (options?.tags?.length) {
1588
+ where.tags = { hasSome: options.tags };
1589
+ }
1590
+ const [records, total] = await Promise.all([
1591
+ model.findMany({
1592
+ where,
1593
+ take: options?.limit || 100,
1594
+ skip: options?.offset || 0,
1595
+ orderBy: { createdAt: "desc" }
1596
+ }),
1597
+ model.count({ where })
1598
+ ]);
1599
+ return {
1600
+ subscribers: records.map((r) => mapToSubscriber(r)),
1601
+ total
1602
+ };
1603
+ },
1604
+ async deleteSubscriber(email) {
1605
+ try {
1606
+ await model.delete({
1607
+ where: { email: email.toLowerCase() }
1608
+ });
1609
+ return true;
1610
+ } catch {
1611
+ return false;
1612
+ }
1613
+ },
1614
+ async updateSubscriber(email, data) {
1615
+ try {
1616
+ const record = await model.update({
1617
+ where: { email: email.toLowerCase() },
1618
+ data: {
1619
+ ...data.source !== void 0 && { source: data.source },
1620
+ ...data.tags !== void 0 && { tags: data.tags },
1621
+ ...data.metadata !== void 0 && { metadata: data.metadata }
1622
+ }
1623
+ });
1624
+ return mapToSubscriber(record);
1625
+ } catch {
1626
+ return null;
1627
+ }
1628
+ }
1629
+ };
1630
+ }
1631
+
1632
+ // src/adapters/storage/supabase.ts
1633
+ function mapToSubscriber2(record) {
1634
+ return {
1635
+ id: record.id,
1636
+ email: record.email,
1637
+ status: record.status,
1638
+ source: record.source,
1639
+ tags: record.tags,
1640
+ metadata: record.metadata,
1641
+ consentIp: record.consent_ip,
1642
+ consentAt: record.consent_at ? new Date(record.consent_at) : void 0,
1643
+ confirmedAt: record.confirmed_at ? new Date(record.confirmed_at) : void 0,
1644
+ unsubscribedAt: record.unsubscribed_at ? new Date(record.unsubscribed_at) : void 0,
1645
+ createdAt: new Date(record.created_at),
1646
+ updatedAt: new Date(record.updated_at)
1647
+ };
1648
+ }
1649
+ function createSupabaseAdapter(config) {
1650
+ const { supabase, tableName = "newsletter_subscribers" } = config;
1651
+ return {
1652
+ async createSubscriber(input, token) {
1653
+ const confirmToken = token || generateToken();
1654
+ const email = input.email.toLowerCase();
1655
+ const { data, error } = await supabase.from(tableName).upsert(
1656
+ {
1657
+ email,
1658
+ token: confirmToken,
1659
+ status: "pending",
1660
+ source: input.source,
1661
+ tags: input.tags || [],
1662
+ metadata: input.metadata,
1663
+ consent_ip: input.ip,
1664
+ consent_at: (/* @__PURE__ */ new Date()).toISOString(),
1665
+ unsubscribed_at: null
1666
+ },
1667
+ { onConflict: "email" }
1668
+ );
1669
+ if (error) throw error;
1670
+ const { data: record } = await supabase.from(tableName).select().eq("email", email).single();
1671
+ return mapToSubscriber2(record);
1672
+ },
1673
+ async getSubscriberByEmail(email) {
1674
+ const { data, error } = await supabase.from(tableName).select().eq("email", email.toLowerCase()).single();
1675
+ if (error || !data) return null;
1676
+ return mapToSubscriber2(data);
1677
+ },
1678
+ async getSubscriberByToken(token) {
1679
+ const { data, error } = await supabase.from(tableName).select().eq("token", token).single();
1680
+ if (error || !data) return null;
1681
+ return mapToSubscriber2(data);
1682
+ },
1683
+ async confirmSubscriber(token) {
1684
+ const { error } = await supabase.from(tableName).update({
1685
+ status: "confirmed",
1686
+ confirmed_at: (/* @__PURE__ */ new Date()).toISOString(),
1687
+ token: null
1688
+ }).eq("token", token);
1689
+ if (error) return null;
1690
+ const { data } = await supabase.from(tableName).select().eq("token", null).single();
1691
+ return data ? mapToSubscriber2(data) : null;
1692
+ },
1693
+ async unsubscribe(email) {
1694
+ const { error } = await supabase.from(tableName).update({
1695
+ status: "unsubscribed",
1696
+ unsubscribed_at: (/* @__PURE__ */ new Date()).toISOString()
1697
+ }).eq("email", email.toLowerCase());
1698
+ return !error;
1699
+ },
1700
+ async listSubscribers(options) {
1701
+ const limit = options?.limit || 100;
1702
+ const offset = options?.offset || 0;
1703
+ let query = supabase.from(tableName).select();
1704
+ if (options?.status) {
1705
+ query = query.eq("status", options.status);
1706
+ }
1707
+ if (options?.source) {
1708
+ query = query.eq("source", options.source);
1709
+ }
1710
+ if (options?.tags?.length) {
1711
+ query = query.contains("tags", options.tags);
1712
+ }
1713
+ const { data, error } = await query.order("created_at", { ascending: false }).range(offset, offset + limit - 1);
1714
+ if (error) throw error;
1715
+ return {
1716
+ subscribers: (data || []).map((r) => mapToSubscriber2(r)),
1717
+ total: (data || []).length
1718
+ // Note: Supabase doesn't return total count easily
1719
+ };
1720
+ },
1721
+ async deleteSubscriber(email) {
1722
+ const { error } = await supabase.from(tableName).delete().eq("email", email.toLowerCase());
1723
+ return !error;
1724
+ },
1725
+ async updateSubscriber(email, data) {
1726
+ const updateData = {};
1727
+ if (data.source !== void 0) updateData.source = data.source;
1728
+ if (data.tags !== void 0) updateData.tags = data.tags;
1729
+ if (data.metadata !== void 0) updateData.metadata = data.metadata;
1730
+ const { error } = await supabase.from(tableName).update(updateData).eq("email", email.toLowerCase());
1731
+ if (error) return null;
1732
+ return this.getSubscriberByEmail(email);
1733
+ }
1734
+ };
1735
+ }
1736
+ export {
1737
+ NewsletterBlock,
1738
+ NewsletterCard,
1739
+ NewsletterFooter,
1740
+ NewsletterForm,
1741
+ NewsletterInline,
1742
+ NewsletterModalContent,
1743
+ createMailchimpAdapter,
1744
+ createMailchimpStorageAdapter,
1745
+ createMemoryAdapter,
1746
+ createNewsletterHandlers,
1747
+ createNodemailerAdapter,
1748
+ createNoopAdapter,
1749
+ createPrismaAdapter,
1750
+ createRateLimiter,
1751
+ createResendAdapter,
1752
+ createSupabaseAdapter,
1753
+ generateToken,
1754
+ generateUrlSafeToken,
1755
+ getClientIP,
1756
+ isDisposableEmail,
1757
+ isValidEmail,
1758
+ normalizeEmail,
1759
+ sanitizeString,
1760
+ useNewsletter
1761
+ };
1762
+ //# sourceMappingURL=index.js.map