@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.
@@ -0,0 +1,505 @@
1
+ "use client";
2
+
3
+ // src/components/newsletter-form.tsx
4
+ import { useState, useCallback } from "react";
5
+
6
+ // src/utils/cn.ts
7
+ function cn(...inputs) {
8
+ return inputs.filter(Boolean).join(" ");
9
+ }
10
+
11
+ // src/components/newsletter-form.tsx
12
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
+ function NewsletterForm({
14
+ endpoint = "/api/newsletter/subscribe",
15
+ source,
16
+ tags,
17
+ honeypotField = "website",
18
+ className,
19
+ formClassName,
20
+ inputClassName,
21
+ buttonClassName,
22
+ messageClassName,
23
+ placeholder = "Enter your email",
24
+ buttonText = "Subscribe",
25
+ loadingText = "Subscribing...",
26
+ successMessage,
27
+ errorMessage,
28
+ showMessage = true,
29
+ onSuccess,
30
+ onError,
31
+ onSubmit,
32
+ disabled = false,
33
+ metadata
34
+ }) {
35
+ const [state, setState] = useState({
36
+ status: "idle",
37
+ message: "",
38
+ email: ""
39
+ });
40
+ const handleSubmit = useCallback(
41
+ async (e) => {
42
+ e.preventDefault();
43
+ const formData = new FormData(e.currentTarget);
44
+ const email = formData.get("email");
45
+ const honeypot = formData.get(honeypotField);
46
+ if (honeypot) {
47
+ setState({
48
+ status: "success",
49
+ message: successMessage || "Thanks for subscribing!",
50
+ email: ""
51
+ });
52
+ return;
53
+ }
54
+ if (!email || !email.includes("@")) {
55
+ setState({
56
+ status: "error",
57
+ message: "Please enter a valid email address.",
58
+ email
59
+ });
60
+ onError?.("Please enter a valid email address.");
61
+ return;
62
+ }
63
+ setState({ status: "loading", message: "", email });
64
+ onSubmit?.(email);
65
+ try {
66
+ const response = await fetch(endpoint, {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify({
70
+ email,
71
+ source,
72
+ tags,
73
+ metadata
74
+ })
75
+ });
76
+ const data = await response.json();
77
+ if (response.ok && data.success) {
78
+ const message = successMessage || data.message || "Thanks for subscribing!";
79
+ setState({
80
+ status: "success",
81
+ message,
82
+ email: ""
83
+ });
84
+ onSuccess?.(email, message);
85
+ } else {
86
+ const message = errorMessage || data.message || "Something went wrong. Please try again.";
87
+ setState({
88
+ status: "error",
89
+ message,
90
+ email
91
+ });
92
+ onError?.(message);
93
+ }
94
+ } catch (error) {
95
+ const message = errorMessage || "Network error. Please try again.";
96
+ setState({
97
+ status: "error",
98
+ message,
99
+ email
100
+ });
101
+ onError?.(message);
102
+ }
103
+ },
104
+ [endpoint, source, tags, metadata, honeypotField, successMessage, errorMessage, onSuccess, onError, onSubmit]
105
+ );
106
+ const isLoading = state.status === "loading";
107
+ const isDisabled = disabled || isLoading;
108
+ return /* @__PURE__ */ jsxs("div", { className: cn("w-full", className), children: [
109
+ /* @__PURE__ */ jsxs(
110
+ "form",
111
+ {
112
+ onSubmit: handleSubmit,
113
+ className: cn("flex flex-col sm:flex-row gap-2", formClassName),
114
+ children: [
115
+ /* @__PURE__ */ jsx(
116
+ "input",
117
+ {
118
+ type: "text",
119
+ name: honeypotField,
120
+ autoComplete: "off",
121
+ tabIndex: -1,
122
+ "aria-hidden": "true",
123
+ className: "absolute -left-[9999px] opacity-0 pointer-events-none"
124
+ }
125
+ ),
126
+ /* @__PURE__ */ jsx(
127
+ "input",
128
+ {
129
+ type: "email",
130
+ name: "email",
131
+ placeholder,
132
+ required: true,
133
+ disabled: isDisabled,
134
+ defaultValue: state.email,
135
+ "aria-label": "Email address",
136
+ "aria-describedby": state.message ? "newsletter-message" : void 0,
137
+ className: cn(
138
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
139
+ "ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium",
140
+ "placeholder:text-muted-foreground",
141
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
142
+ "disabled:cursor-not-allowed disabled:opacity-50",
143
+ inputClassName
144
+ )
145
+ }
146
+ ),
147
+ /* @__PURE__ */ jsx(
148
+ "button",
149
+ {
150
+ type: "submit",
151
+ disabled: isDisabled,
152
+ className: cn(
153
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium",
154
+ "ring-offset-background transition-colors",
155
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
156
+ "disabled:pointer-events-none disabled:opacity-50",
157
+ "bg-primary text-primary-foreground hover:bg-primary/90",
158
+ "h-10 px-4 py-2",
159
+ "min-w-[120px]",
160
+ buttonClassName
161
+ ),
162
+ children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [
163
+ /* @__PURE__ */ jsxs(
164
+ "svg",
165
+ {
166
+ className: "mr-2 h-4 w-4 animate-spin",
167
+ xmlns: "http://www.w3.org/2000/svg",
168
+ fill: "none",
169
+ viewBox: "0 0 24 24",
170
+ children: [
171
+ /* @__PURE__ */ jsx(
172
+ "circle",
173
+ {
174
+ className: "opacity-25",
175
+ cx: "12",
176
+ cy: "12",
177
+ r: "10",
178
+ stroke: "currentColor",
179
+ strokeWidth: "4"
180
+ }
181
+ ),
182
+ /* @__PURE__ */ jsx(
183
+ "path",
184
+ {
185
+ className: "opacity-75",
186
+ fill: "currentColor",
187
+ 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"
188
+ }
189
+ )
190
+ ]
191
+ }
192
+ ),
193
+ loadingText
194
+ ] }) : buttonText
195
+ }
196
+ )
197
+ ]
198
+ }
199
+ ),
200
+ showMessage && state.message && /* @__PURE__ */ jsx(
201
+ "p",
202
+ {
203
+ id: "newsletter-message",
204
+ role: state.status === "error" ? "alert" : "status",
205
+ className: cn(
206
+ "mt-2 text-sm",
207
+ state.status === "success" && "text-green-600 dark:text-green-400",
208
+ state.status === "error" && "text-red-600 dark:text-red-400",
209
+ messageClassName
210
+ ),
211
+ children: state.message
212
+ }
213
+ )
214
+ ] });
215
+ }
216
+
217
+ // src/components/newsletter-blocks.tsx
218
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
219
+ function NewsletterBlock({
220
+ title = "Subscribe to our newsletter",
221
+ description = "Get the latest updates delivered directly to your inbox.",
222
+ containerClassName,
223
+ titleClassName,
224
+ descriptionClassName,
225
+ ...formProps
226
+ }) {
227
+ return /* @__PURE__ */ jsx2(
228
+ "section",
229
+ {
230
+ className: cn(
231
+ "w-full py-12 md:py-16 lg:py-20",
232
+ "bg-muted/50",
233
+ containerClassName
234
+ ),
235
+ 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: [
236
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
237
+ /* @__PURE__ */ jsx2(
238
+ "h2",
239
+ {
240
+ className: cn(
241
+ "text-2xl font-bold tracking-tight sm:text-3xl md:text-4xl",
242
+ titleClassName
243
+ ),
244
+ children: title
245
+ }
246
+ ),
247
+ /* @__PURE__ */ jsx2(
248
+ "p",
249
+ {
250
+ className: cn(
251
+ "max-w-[600px] text-muted-foreground md:text-lg",
252
+ descriptionClassName
253
+ ),
254
+ children: description
255
+ }
256
+ )
257
+ ] }),
258
+ /* @__PURE__ */ jsx2("div", { className: "w-full max-w-md", children: /* @__PURE__ */ jsx2(NewsletterForm, { ...formProps }) })
259
+ ] }) })
260
+ }
261
+ );
262
+ }
263
+ function NewsletterCard({
264
+ title = "Newsletter",
265
+ description = "Subscribe to get updates.",
266
+ cardClassName,
267
+ titleClassName,
268
+ descriptionClassName,
269
+ ...formProps
270
+ }) {
271
+ return /* @__PURE__ */ jsx2(
272
+ "div",
273
+ {
274
+ className: cn(
275
+ "rounded-lg border bg-card text-card-foreground shadow-sm",
276
+ "p-6",
277
+ cardClassName
278
+ ),
279
+ children: /* @__PURE__ */ jsxs2("div", { className: "space-y-4", children: [
280
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-1.5", children: [
281
+ /* @__PURE__ */ jsx2(
282
+ "h3",
283
+ {
284
+ className: cn(
285
+ "text-lg font-semibold leading-none tracking-tight",
286
+ titleClassName
287
+ ),
288
+ children: title
289
+ }
290
+ ),
291
+ /* @__PURE__ */ jsx2(
292
+ "p",
293
+ {
294
+ className: cn(
295
+ "text-sm text-muted-foreground",
296
+ descriptionClassName
297
+ ),
298
+ children: description
299
+ }
300
+ )
301
+ ] }),
302
+ /* @__PURE__ */ jsx2(NewsletterForm, { ...formProps })
303
+ ] })
304
+ }
305
+ );
306
+ }
307
+ function NewsletterInline({
308
+ label,
309
+ labelClassName,
310
+ ...formProps
311
+ }) {
312
+ return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2", children: [
313
+ label && /* @__PURE__ */ jsx2(
314
+ "label",
315
+ {
316
+ className: cn(
317
+ "text-sm font-medium leading-none",
318
+ labelClassName
319
+ ),
320
+ children: label
321
+ }
322
+ ),
323
+ /* @__PURE__ */ jsx2(NewsletterForm, { ...formProps })
324
+ ] });
325
+ }
326
+ function NewsletterModalContent({
327
+ title = "Subscribe to our newsletter",
328
+ description = "We'll send you the best content, no spam.",
329
+ titleClassName,
330
+ descriptionClassName,
331
+ ...formProps
332
+ }) {
333
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-4", children: [
334
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-2 text-center", children: [
335
+ /* @__PURE__ */ jsx2(
336
+ "h2",
337
+ {
338
+ className: cn(
339
+ "text-lg font-semibold leading-none tracking-tight",
340
+ titleClassName
341
+ ),
342
+ children: title
343
+ }
344
+ ),
345
+ /* @__PURE__ */ jsx2(
346
+ "p",
347
+ {
348
+ className: cn(
349
+ "text-sm text-muted-foreground",
350
+ descriptionClassName
351
+ ),
352
+ children: description
353
+ }
354
+ )
355
+ ] }),
356
+ /* @__PURE__ */ jsx2(
357
+ NewsletterForm,
358
+ {
359
+ ...formProps,
360
+ formClassName: "flex-col",
361
+ buttonClassName: "w-full"
362
+ }
363
+ )
364
+ ] });
365
+ }
366
+ function NewsletterFooter({
367
+ title = "Newsletter",
368
+ description,
369
+ containerClassName,
370
+ titleClassName,
371
+ descriptionClassName,
372
+ privacyText,
373
+ privacyLink,
374
+ ...formProps
375
+ }) {
376
+ return /* @__PURE__ */ jsxs2("div", { className: cn("space-y-4", containerClassName), children: [
377
+ title && /* @__PURE__ */ jsx2(
378
+ "h3",
379
+ {
380
+ className: cn(
381
+ "text-sm font-semibold uppercase tracking-wider",
382
+ titleClassName
383
+ ),
384
+ children: title
385
+ }
386
+ ),
387
+ description && /* @__PURE__ */ jsx2(
388
+ "p",
389
+ {
390
+ className: cn(
391
+ "text-sm text-muted-foreground",
392
+ descriptionClassName
393
+ ),
394
+ children: description
395
+ }
396
+ ),
397
+ /* @__PURE__ */ jsx2(NewsletterForm, { ...formProps }),
398
+ privacyText && /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground", children: privacyLink ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
399
+ privacyText.replace(/\.$/, ""),
400
+ ".",
401
+ " ",
402
+ /* @__PURE__ */ jsx2("a", { href: privacyLink, className: "underline hover:text-foreground", children: "Privacy Policy" })
403
+ ] }) : privacyText })
404
+ ] });
405
+ }
406
+
407
+ // src/hooks/use-newsletter.ts
408
+ import { useState as useState2, useCallback as useCallback2 } from "react";
409
+ function useNewsletter(options = {}) {
410
+ const {
411
+ endpoint = "/api/newsletter/subscribe",
412
+ source,
413
+ tags,
414
+ metadata,
415
+ onSuccess,
416
+ onError
417
+ } = options;
418
+ const [state, setState] = useState2({
419
+ status: "idle",
420
+ message: "",
421
+ email: null
422
+ });
423
+ const subscribe = useCallback2(
424
+ async (email) => {
425
+ if (!email || !email.includes("@")) {
426
+ const errorMsg = "Please enter a valid email address.";
427
+ setState({
428
+ status: "error",
429
+ message: errorMsg,
430
+ email
431
+ });
432
+ onError?.(errorMsg);
433
+ return false;
434
+ }
435
+ setState({ status: "loading", message: "", email });
436
+ try {
437
+ const response = await fetch(endpoint, {
438
+ method: "POST",
439
+ headers: { "Content-Type": "application/json" },
440
+ body: JSON.stringify({
441
+ email,
442
+ source,
443
+ tags,
444
+ metadata
445
+ })
446
+ });
447
+ const data = await response.json();
448
+ if (response.ok && data.success) {
449
+ const message = data.message || "Thanks for subscribing!";
450
+ setState({
451
+ status: "success",
452
+ message,
453
+ email
454
+ });
455
+ onSuccess?.(email, message);
456
+ return true;
457
+ } else {
458
+ const message = data.message || "Something went wrong. Please try again.";
459
+ setState({
460
+ status: "error",
461
+ message,
462
+ email
463
+ });
464
+ onError?.(message);
465
+ return false;
466
+ }
467
+ } catch (error) {
468
+ const message = "Network error. Please try again.";
469
+ setState({
470
+ status: "error",
471
+ message,
472
+ email
473
+ });
474
+ onError?.(message);
475
+ return false;
476
+ }
477
+ },
478
+ [endpoint, source, tags, metadata, onSuccess, onError]
479
+ );
480
+ const reset = useCallback2(() => {
481
+ setState({
482
+ status: "idle",
483
+ message: "",
484
+ email: null
485
+ });
486
+ }, []);
487
+ return {
488
+ ...state,
489
+ subscribe,
490
+ reset,
491
+ isLoading: state.status === "loading",
492
+ isSuccess: state.status === "success",
493
+ isError: state.status === "error"
494
+ };
495
+ }
496
+ export {
497
+ NewsletterBlock,
498
+ NewsletterCard,
499
+ NewsletterFooter,
500
+ NewsletterForm,
501
+ NewsletterInline,
502
+ NewsletterModalContent,
503
+ useNewsletter
504
+ };
505
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/newsletter-form.tsx","../../src/utils/cn.ts","../../src/components/newsletter-blocks.tsx","../../src/hooks/use-newsletter.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { useState, useCallback, FormEvent } from 'react';\nimport type { NewsletterFormProps, NewsletterFormState } from '../types';\nimport { cn } from '../utils/cn';\n\n/**\n * Newsletter subscription form component\n * \n * Designed to work with shadcn/ui but includes default Tailwind styles.\n * \n * @example\n * ```tsx\n * // Basic usage\n * <NewsletterForm endpoint=\"/api/newsletter/subscribe\" />\n * \n * // With all options\n * <NewsletterForm\n * endpoint=\"/api/newsletter/subscribe\"\n * source=\"footer\"\n * tags={['marketing']}\n * placeholder=\"Enter your email\"\n * buttonText=\"Subscribe\"\n * successMessage=\"Check your inbox!\"\n * onSuccess={(email) => console.log('Subscribed:', email)}\n * onError={(error) => console.error('Error:', error)}\n * className=\"max-w-md\"\n * />\n * ```\n */\nexport function NewsletterForm({\n endpoint = '/api/newsletter/subscribe',\n source,\n tags,\n honeypotField = 'website',\n className,\n formClassName,\n inputClassName,\n buttonClassName,\n messageClassName,\n placeholder = 'Enter your email',\n buttonText = 'Subscribe',\n loadingText = 'Subscribing...',\n successMessage,\n errorMessage,\n showMessage = true,\n onSuccess,\n onError,\n onSubmit,\n disabled = false,\n metadata,\n}: NewsletterFormProps) {\n const [state, setState] = useState<NewsletterFormState>({\n status: 'idle',\n message: '',\n email: '',\n });\n\n const handleSubmit = useCallback(\n async (e: FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n\n const formData = new FormData(e.currentTarget);\n const email = formData.get('email') as string;\n const honeypot = formData.get(honeypotField) as string;\n\n // If honeypot is filled, silently \"succeed\" (bot detection)\n if (honeypot) {\n setState({\n status: 'success',\n message: successMessage || 'Thanks for subscribing!',\n email: '',\n });\n return;\n }\n\n if (!email || !email.includes('@')) {\n setState({\n status: 'error',\n message: 'Please enter a valid email address.',\n email,\n });\n onError?.('Please enter a valid email address.');\n return;\n }\n\n setState({ status: 'loading', message: '', email });\n onSubmit?.(email);\n\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n email,\n source,\n tags,\n metadata,\n }),\n });\n\n const data = await response.json();\n\n if (response.ok && data.success) {\n const message = successMessage || data.message || 'Thanks for subscribing!';\n setState({\n status: 'success',\n message,\n email: '',\n });\n onSuccess?.(email, message);\n } else {\n const message = errorMessage || data.message || 'Something went wrong. Please try again.';\n setState({\n status: 'error',\n message,\n email,\n });\n onError?.(message);\n }\n } catch (error) {\n const message = errorMessage || 'Network error. Please try again.';\n setState({\n status: 'error',\n message,\n email,\n });\n onError?.(message);\n }\n },\n [endpoint, source, tags, metadata, honeypotField, successMessage, errorMessage, onSuccess, onError, onSubmit]\n );\n\n const isLoading = state.status === 'loading';\n const isDisabled = disabled || isLoading;\n\n return (\n <div className={cn('w-full', className)}>\n <form\n onSubmit={handleSubmit}\n className={cn('flex flex-col sm:flex-row gap-2', formClassName)}\n >\n {/* Honeypot field - hidden from users, visible to bots */}\n <input\n type=\"text\"\n name={honeypotField}\n autoComplete=\"off\"\n tabIndex={-1}\n aria-hidden=\"true\"\n className=\"absolute -left-[9999px] opacity-0 pointer-events-none\"\n />\n\n <input\n type=\"email\"\n name=\"email\"\n placeholder={placeholder}\n required\n disabled={isDisabled}\n defaultValue={state.email}\n aria-label=\"Email address\"\n aria-describedby={state.message ? 'newsletter-message' : undefined}\n className={cn(\n 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm',\n 'ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium',\n 'placeholder:text-muted-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n inputClassName\n )}\n />\n\n <button\n type=\"submit\"\n disabled={isDisabled}\n className={cn(\n 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium',\n 'ring-offset-background transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n 'disabled:pointer-events-none disabled:opacity-50',\n 'bg-primary text-primary-foreground hover:bg-primary/90',\n 'h-10 px-4 py-2',\n 'min-w-[120px]',\n buttonClassName\n )}\n >\n {isLoading ? (\n <>\n <svg\n className=\"mr-2 h-4 w-4 animate-spin\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n 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\"\n />\n </svg>\n {loadingText}\n </>\n ) : (\n buttonText\n )}\n </button>\n </form>\n\n {showMessage && state.message && (\n <p\n id=\"newsletter-message\"\n role={state.status === 'error' ? 'alert' : 'status'}\n className={cn(\n 'mt-2 text-sm',\n state.status === 'success' && 'text-green-600 dark:text-green-400',\n state.status === 'error' && 'text-red-600 dark:text-red-400',\n messageClassName\n )}\n >\n {state.message}\n </p>\n )}\n </div>\n );\n}\n","/**\n * Utility for merging class names\n * Compatible with shadcn/ui's cn function\n */\nexport function cn(...inputs: (string | undefined | null | false)[]): string {\n return inputs.filter(Boolean).join(' ');\n}\n","'use client';\n\nimport * as React from 'react';\nimport { NewsletterForm } from './newsletter-form';\nimport type { NewsletterFormProps } from '../types';\nimport { cn } from '../utils/cn';\n\n// ============================================================================\n// Newsletter Block - Full width section\n// ============================================================================\n\ninterface NewsletterBlockProps extends NewsletterFormProps {\n title?: string;\n description?: string;\n containerClassName?: string;\n titleClassName?: string;\n descriptionClassName?: string;\n}\n\n/**\n * Full-width newsletter subscription block\n * \n * @example\n * ```tsx\n * <NewsletterBlock\n * endpoint=\"/api/newsletter/subscribe\"\n * title=\"Stay updated\"\n * description=\"Get the latest news delivered to your inbox.\"\n * source=\"footer\"\n * />\n * ```\n */\nexport function NewsletterBlock({\n title = 'Subscribe to our newsletter',\n description = 'Get the latest updates delivered directly to your inbox.',\n containerClassName,\n titleClassName,\n descriptionClassName,\n ...formProps\n}: NewsletterBlockProps) {\n return (\n <section\n className={cn(\n 'w-full py-12 md:py-16 lg:py-20',\n 'bg-muted/50',\n containerClassName\n )}\n >\n <div className=\"container mx-auto px-4 md:px-6\">\n <div className=\"flex flex-col items-center justify-center space-y-4 text-center\">\n <div className=\"space-y-2\">\n <h2\n className={cn(\n 'text-2xl font-bold tracking-tight sm:text-3xl md:text-4xl',\n titleClassName\n )}\n >\n {title}\n </h2>\n <p\n className={cn(\n 'max-w-[600px] text-muted-foreground md:text-lg',\n descriptionClassName\n )}\n >\n {description}\n </p>\n </div>\n <div className=\"w-full max-w-md\">\n <NewsletterForm {...formProps} />\n </div>\n </div>\n </div>\n </section>\n );\n}\n\n// ============================================================================\n// Newsletter Card - Contained card variant\n// ============================================================================\n\ninterface NewsletterCardProps extends NewsletterFormProps {\n title?: string;\n description?: string;\n cardClassName?: string;\n titleClassName?: string;\n descriptionClassName?: string;\n}\n\n/**\n * Card-style newsletter subscription component\n * \n * @example\n * ```tsx\n * <NewsletterCard\n * endpoint=\"/api/newsletter/subscribe\"\n * title=\"Join the community\"\n * description=\"Be the first to know about new features.\"\n * source=\"sidebar\"\n * />\n * ```\n */\nexport function NewsletterCard({\n title = 'Newsletter',\n description = 'Subscribe to get updates.',\n cardClassName,\n titleClassName,\n descriptionClassName,\n ...formProps\n}: NewsletterCardProps) {\n return (\n <div\n className={cn(\n 'rounded-lg border bg-card text-card-foreground shadow-sm',\n 'p-6',\n cardClassName\n )}\n >\n <div className=\"space-y-4\">\n <div className=\"space-y-1.5\">\n <h3\n className={cn(\n 'text-lg font-semibold leading-none tracking-tight',\n titleClassName\n )}\n >\n {title}\n </h3>\n <p\n className={cn(\n 'text-sm text-muted-foreground',\n descriptionClassName\n )}\n >\n {description}\n </p>\n </div>\n <NewsletterForm {...formProps} />\n </div>\n </div>\n );\n}\n\n// ============================================================================\n// Newsletter Inline - Minimal inline variant\n// ============================================================================\n\ninterface NewsletterInlineProps extends NewsletterFormProps {\n label?: string;\n labelClassName?: string;\n}\n\n/**\n * Inline newsletter subscription component\n * \n * @example\n * ```tsx\n * // In a footer\n * <NewsletterInline\n * endpoint=\"/api/newsletter/subscribe\"\n * label=\"Subscribe:\"\n * source=\"footer\"\n * />\n * ```\n */\nexport function NewsletterInline({\n label,\n labelClassName,\n ...formProps\n}: NewsletterInlineProps) {\n return (\n <div className=\"flex flex-col gap-2\">\n {label && (\n <label\n className={cn(\n 'text-sm font-medium leading-none',\n labelClassName\n )}\n >\n {label}\n </label>\n )}\n <NewsletterForm {...formProps} />\n </div>\n );\n}\n\n// ============================================================================\n// Newsletter Modal - Dialog/modal variant (controlled externally)\n// ============================================================================\n\ninterface NewsletterModalContentProps extends NewsletterFormProps {\n title?: string;\n description?: string;\n titleClassName?: string;\n descriptionClassName?: string;\n}\n\n/**\n * Newsletter content for use inside a modal/dialog\n * \n * Use with shadcn/ui Dialog component:\n * \n * @example\n * ```tsx\n * import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';\n * import { NewsletterModalContent } from '@volchok/newsletter-kit/components';\n * \n * <Dialog>\n * <DialogTrigger asChild>\n * <Button>Subscribe</Button>\n * </DialogTrigger>\n * <DialogContent>\n * <NewsletterModalContent\n * endpoint=\"/api/newsletter/subscribe\"\n * title=\"Join our newsletter\"\n * description=\"Get weekly updates on the latest news.\"\n * source=\"popup\"\n * onSuccess={() => {\n * // Close modal after success\n * setTimeout(() => setOpen(false), 2000);\n * }}\n * />\n * </DialogContent>\n * </Dialog>\n * ```\n */\nexport function NewsletterModalContent({\n title = 'Subscribe to our newsletter',\n description = \"We'll send you the best content, no spam.\",\n titleClassName,\n descriptionClassName,\n ...formProps\n}: NewsletterModalContentProps) {\n return (\n <div className=\"space-y-4\">\n <div className=\"space-y-2 text-center\">\n <h2\n className={cn(\n 'text-lg font-semibold leading-none tracking-tight',\n titleClassName\n )}\n >\n {title}\n </h2>\n <p\n className={cn(\n 'text-sm text-muted-foreground',\n descriptionClassName\n )}\n >\n {description}\n </p>\n </div>\n <NewsletterForm\n {...formProps}\n formClassName=\"flex-col\"\n buttonClassName=\"w-full\"\n />\n </div>\n );\n}\n\n// ============================================================================\n// Newsletter Footer - Optimized for site footers\n// ============================================================================\n\ninterface NewsletterFooterProps extends NewsletterFormProps {\n title?: string;\n description?: string;\n containerClassName?: string;\n titleClassName?: string;\n descriptionClassName?: string;\n privacyText?: string;\n privacyLink?: string;\n}\n\n/**\n * Footer-optimized newsletter subscription component\n * \n * @example\n * ```tsx\n * <footer>\n * <NewsletterFooter\n * endpoint=\"/api/newsletter/subscribe\"\n * title=\"Newsletter\"\n * description=\"Stay up to date with our latest news.\"\n * source=\"footer\"\n * privacyText=\"We respect your privacy.\"\n * privacyLink=\"/privacy\"\n * />\n * </footer>\n * ```\n */\nexport function NewsletterFooter({\n title = 'Newsletter',\n description,\n containerClassName,\n titleClassName,\n descriptionClassName,\n privacyText,\n privacyLink,\n ...formProps\n}: NewsletterFooterProps) {\n return (\n <div className={cn('space-y-4', containerClassName)}>\n {title && (\n <h3\n className={cn(\n 'text-sm font-semibold uppercase tracking-wider',\n titleClassName\n )}\n >\n {title}\n </h3>\n )}\n {description && (\n <p\n className={cn(\n 'text-sm text-muted-foreground',\n descriptionClassName\n )}\n >\n {description}\n </p>\n )}\n <NewsletterForm {...formProps} />\n {privacyText && (\n <p className=\"text-xs text-muted-foreground\">\n {privacyLink ? (\n <>\n {privacyText.replace(/\\.$/, '')}.{' '}\n <a href={privacyLink} className=\"underline hover:text-foreground\">\n Privacy Policy\n </a>\n </>\n ) : (\n privacyText\n )}\n </p>\n )}\n </div>\n );\n}\n","'use client';\n\nimport { useState, useCallback } from 'react';\n\ninterface UseNewsletterOptions {\n endpoint?: string;\n source?: string;\n tags?: string[];\n metadata?: Record<string, unknown>;\n onSuccess?: (email: string, message: string) => void;\n onError?: (error: string) => void;\n}\n\ninterface UseNewsletterState {\n status: 'idle' | 'loading' | 'success' | 'error';\n message: string;\n email: string | null;\n}\n\ninterface UseNewsletterReturn extends UseNewsletterState {\n subscribe: (email: string) => Promise<boolean>;\n reset: () => void;\n isLoading: boolean;\n isSuccess: boolean;\n isError: boolean;\n}\n\n/**\n * React hook for programmatic newsletter subscription\n * \n * @example\n * ```tsx\n * const { subscribe, isLoading, isSuccess, message } = useNewsletter({\n * endpoint: '/api/newsletter/subscribe',\n * source: 'custom-form',\n * onSuccess: (email) => {\n * analytics.track('newsletter_subscribed', { email });\n * },\n * });\n * \n * const handleSubmit = async (email: string) => {\n * const success = await subscribe(email);\n * if (success) {\n * // Show confetti or something\n * }\n * };\n * ```\n */\nexport function useNewsletter(options: UseNewsletterOptions = {}): UseNewsletterReturn {\n const {\n endpoint = '/api/newsletter/subscribe',\n source,\n tags,\n metadata,\n onSuccess,\n onError,\n } = options;\n\n const [state, setState] = useState<UseNewsletterState>({\n status: 'idle',\n message: '',\n email: null,\n });\n\n const subscribe = useCallback(\n async (email: string): Promise<boolean> => {\n if (!email || !email.includes('@')) {\n const errorMsg = 'Please enter a valid email address.';\n setState({\n status: 'error',\n message: errorMsg,\n email,\n });\n onError?.(errorMsg);\n return false;\n }\n\n setState({ status: 'loading', message: '', email });\n\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n email,\n source,\n tags,\n metadata,\n }),\n });\n\n const data = await response.json();\n\n if (response.ok && data.success) {\n const message = data.message || 'Thanks for subscribing!';\n setState({\n status: 'success',\n message,\n email,\n });\n onSuccess?.(email, message);\n return true;\n } else {\n const message = data.message || 'Something went wrong. Please try again.';\n setState({\n status: 'error',\n message,\n email,\n });\n onError?.(message);\n return false;\n }\n } catch (error) {\n const message = 'Network error. Please try again.';\n setState({\n status: 'error',\n message,\n email,\n });\n onError?.(message);\n return false;\n }\n },\n [endpoint, source, tags, metadata, onSuccess, onError]\n );\n\n const reset = useCallback(() => {\n setState({\n status: 'idle',\n message: '',\n email: null,\n });\n }, []);\n\n return {\n ...state,\n subscribe,\n reset,\n isLoading: state.status === 'loading',\n isSuccess: state.status === 'success',\n isError: state.status === 'error',\n };\n}\n"],"mappings":";;;AAGA,SAAS,UAAU,mBAA8B;;;ACC1C,SAAS,MAAM,QAAuD;AAC3E,SAAO,OAAO,OAAO,OAAO,EAAE,KAAK,GAAG;AACxC;;;AD0IQ,SA2CI,UA3CJ,KA4CM,YA5CN;AAjHD,SAAS,eAAe;AAAA,EAC7B,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,aAAa;AAAA,EACb,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AACF,GAAwB;AACtB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA8B;AAAA,IACtD,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,OAAO;AAAA,EACT,CAAC;AAED,QAAM,eAAe;AAAA,IACnB,OAAO,MAAkC;AACvC,QAAE,eAAe;AAEjB,YAAM,WAAW,IAAI,SAAS,EAAE,aAAa;AAC7C,YAAM,QAAQ,SAAS,IAAI,OAAO;AAClC,YAAM,WAAW,SAAS,IAAI,aAAa;AAG3C,UAAI,UAAU;AACZ,iBAAS;AAAA,UACP,QAAQ;AAAA,UACR,SAAS,kBAAkB;AAAA,UAC3B,OAAO;AAAA,QACT,CAAC;AACD;AAAA,MACF;AAEA,UAAI,CAAC,SAAS,CAAC,MAAM,SAAS,GAAG,GAAG;AAClC,iBAAS;AAAA,UACP,QAAQ;AAAA,UACR,SAAS;AAAA,UACT;AAAA,QACF,CAAC;AACD,kBAAU,qCAAqC;AAC/C;AAAA,MACF;AAEA,eAAS,EAAE,QAAQ,WAAW,SAAS,IAAI,MAAM,CAAC;AAClD,iBAAW,KAAK;AAEhB,UAAI;AACF,cAAM,WAAW,MAAM,MAAM,UAAU;AAAA,UACrC,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU;AAAA,YACnB;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF,CAAC;AAAA,QACH,CAAC;AAED,cAAM,OAAO,MAAM,SAAS,KAAK;AAEjC,YAAI,SAAS,MAAM,KAAK,SAAS;AAC/B,gBAAM,UAAU,kBAAkB,KAAK,WAAW;AAClD,mBAAS;AAAA,YACP,QAAQ;AAAA,YACR;AAAA,YACA,OAAO;AAAA,UACT,CAAC;AACD,sBAAY,OAAO,OAAO;AAAA,QAC5B,OAAO;AACL,gBAAM,UAAU,gBAAgB,KAAK,WAAW;AAChD,mBAAS;AAAA,YACP,QAAQ;AAAA,YACR;AAAA,YACA;AAAA,UACF,CAAC;AACD,oBAAU,OAAO;AAAA,QACnB;AAAA,MACF,SAAS,OAAO;AACd,cAAM,UAAU,gBAAgB;AAChC,iBAAS;AAAA,UACP,QAAQ;AAAA,UACR;AAAA,UACA;AAAA,QACF,CAAC;AACD,kBAAU,OAAO;AAAA,MACnB;AAAA,IACF;AAAA,IACA,CAAC,UAAU,QAAQ,MAAM,UAAU,eAAe,gBAAgB,cAAc,WAAW,SAAS,QAAQ;AAAA,EAC9G;AAEA,QAAM,YAAY,MAAM,WAAW;AACnC,QAAM,aAAa,YAAY;AAE/B,SACE,qBAAC,SAAI,WAAW,GAAG,UAAU,SAAS,GACpC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,UAAU;AAAA,QACV,WAAW,GAAG,mCAAmC,aAAa;AAAA,QAG9D;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAM;AAAA,cACN,cAAa;AAAA,cACb,UAAU;AAAA,cACV,eAAY;AAAA,cACZ,WAAU;AAAA;AAAA,UACZ;AAAA,UAEA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAK;AAAA,cACL;AAAA,cACA,UAAQ;AAAA,cACR,UAAU;AAAA,cACV,cAAc,MAAM;AAAA,cACpB,cAAW;AAAA,cACX,oBAAkB,MAAM,UAAU,uBAAuB;AAAA,cACzD,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA;AAAA,UACF;AAAA,UAEA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,UAAU;AAAA,cACV,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA,cAEC,sBACC,iCACE;AAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAU;AAAA,oBACV,OAAM;AAAA,oBACN,MAAK;AAAA,oBACL,SAAQ;AAAA,oBAER;AAAA;AAAA,wBAAC;AAAA;AAAA,0BACC,WAAU;AAAA,0BACV,IAAG;AAAA,0BACH,IAAG;AAAA,0BACH,GAAE;AAAA,0BACF,QAAO;AAAA,0BACP,aAAY;AAAA;AAAA,sBACd;AAAA,sBACA;AAAA,wBAAC;AAAA;AAAA,0BACC,WAAU;AAAA,0BACV,MAAK;AAAA,0BACL,GAAE;AAAA;AAAA,sBACJ;AAAA;AAAA;AAAA,gBACF;AAAA,gBACC;AAAA,iBACH,IAEA;AAAA;AAAA,UAEJ;AAAA;AAAA;AAAA,IACF;AAAA,IAEC,eAAe,MAAM,WACpB;AAAA,MAAC;AAAA;AAAA,QACC,IAAG;AAAA,QACH,MAAM,MAAM,WAAW,UAAU,UAAU;AAAA,QAC3C,WAAW;AAAA,UACT;AAAA,UACA,MAAM,WAAW,aAAa;AAAA,UAC9B,MAAM,WAAW,WAAW;AAAA,UAC5B;AAAA,QACF;AAAA,QAEC,gBAAM;AAAA;AAAA,IACT;AAAA,KAEJ;AAEJ;;;AEtLU,SAwRE,YAAAA,WAvRA,OAAAC,MADF,QAAAC,aAAA;AAlBH,SAAS,gBAAgB;AAAA,EAC9B,QAAQ;AAAA,EACR,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyB;AACvB,SACE,gBAAAD;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAEA,0BAAAA,KAAC,SAAI,WAAU,kCACb,0BAAAC,MAAC,SAAI,WAAU,mEACb;AAAA,wBAAAA,MAAC,SAAI,WAAU,aACb;AAAA,0BAAAD;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cAEC;AAAA;AAAA,UACH;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cAEC;AAAA;AAAA,UACH;AAAA,WACF;AAAA,QACA,gBAAAA,KAAC,SAAI,WAAU,mBACb,0BAAAA,KAAC,kBAAgB,GAAG,WAAW,GACjC;AAAA,SACF,GACF;AAAA;AAAA,EACF;AAEJ;AA2BO,SAAS,eAAe;AAAA,EAC7B,QAAQ;AAAA,EACR,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAwB;AACtB,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAEA,0BAAAC,MAAC,SAAI,WAAU,aACb;AAAA,wBAAAA,MAAC,SAAI,WAAU,eACb;AAAA,0BAAAD;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cAEC;AAAA;AAAA,UACH;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cAEC;AAAA;AAAA,UACH;AAAA,WACF;AAAA,QACA,gBAAAA,KAAC,kBAAgB,GAAG,WAAW;AAAA,SACjC;AAAA;AAAA,EACF;AAEJ;AAwBO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA0B;AACxB,SACE,gBAAAC,MAAC,SAAI,WAAU,uBACZ;AAAA,aACC,gBAAAD;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QAEC;AAAA;AAAA,IACH;AAAA,IAEF,gBAAAA,KAAC,kBAAgB,GAAG,WAAW;AAAA,KACjC;AAEJ;AA0CO,SAAS,uBAAuB;AAAA,EACrC,QAAQ;AAAA,EACR,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAgC;AAC9B,SACE,gBAAAC,MAAC,SAAI,WAAU,aACb;AAAA,oBAAAA,MAAC,SAAI,WAAU,yBACb;AAAA,sBAAAD;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA;AAAA,UACF;AAAA,UAEC;AAAA;AAAA,MACH;AAAA,MACA,gBAAAA;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA;AAAA,UACF;AAAA,UAEC;AAAA;AAAA,MACH;AAAA,OACF;AAAA,IACA,gBAAAA;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ,eAAc;AAAA,QACd,iBAAgB;AAAA;AAAA,IAClB;AAAA,KACF;AAEJ;AAiCO,SAAS,iBAAiB;AAAA,EAC/B,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA0B;AACxB,SACE,gBAAAC,MAAC,SAAI,WAAW,GAAG,aAAa,kBAAkB,GAC/C;AAAA,aACC,gBAAAD;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QAEC;AAAA;AAAA,IACH;AAAA,IAED,eACC,gBAAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QAEC;AAAA;AAAA,IACH;AAAA,IAEF,gBAAAA,KAAC,kBAAgB,GAAG,WAAW;AAAA,IAC9B,eACC,gBAAAA,KAAC,OAAE,WAAU,iCACV,wBACC,gBAAAC,MAAAF,WAAA,EACG;AAAA,kBAAY,QAAQ,OAAO,EAAE;AAAA,MAAE;AAAA,MAAE;AAAA,MAClC,gBAAAC,KAAC,OAAE,MAAM,aAAa,WAAU,mCAAkC,4BAElE;AAAA,OACF,IAEA,aAEJ;AAAA,KAEJ;AAEJ;;;ACrVA,SAAS,YAAAE,WAAU,eAAAC,oBAAmB;AA8C/B,SAAS,cAAc,UAAgC,CAAC,GAAwB;AACrF,QAAM;AAAA,IACJ,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,CAAC,OAAO,QAAQ,IAAID,UAA6B;AAAA,IACrD,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,OAAO;AAAA,EACT,CAAC;AAED,QAAM,YAAYC;AAAA,IAChB,OAAO,UAAoC;AACzC,UAAI,CAAC,SAAS,CAAC,MAAM,SAAS,GAAG,GAAG;AAClC,cAAM,WAAW;AACjB,iBAAS;AAAA,UACP,QAAQ;AAAA,UACR,SAAS;AAAA,UACT;AAAA,QACF,CAAC;AACD,kBAAU,QAAQ;AAClB,eAAO;AAAA,MACT;AAEA,eAAS,EAAE,QAAQ,WAAW,SAAS,IAAI,MAAM,CAAC;AAElD,UAAI;AACF,cAAM,WAAW,MAAM,MAAM,UAAU;AAAA,UACrC,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU;AAAA,YACnB;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF,CAAC;AAAA,QACH,CAAC;AAED,cAAM,OAAO,MAAM,SAAS,KAAK;AAEjC,YAAI,SAAS,MAAM,KAAK,SAAS;AAC/B,gBAAM,UAAU,KAAK,WAAW;AAChC,mBAAS;AAAA,YACP,QAAQ;AAAA,YACR;AAAA,YACA;AAAA,UACF,CAAC;AACD,sBAAY,OAAO,OAAO;AAC1B,iBAAO;AAAA,QACT,OAAO;AACL,gBAAM,UAAU,KAAK,WAAW;AAChC,mBAAS;AAAA,YACP,QAAQ;AAAA,YACR;AAAA,YACA;AAAA,UACF,CAAC;AACD,oBAAU,OAAO;AACjB,iBAAO;AAAA,QACT;AAAA,MACF,SAAS,OAAO;AACd,cAAM,UAAU;AAChB,iBAAS;AAAA,UACP,QAAQ;AAAA,UACR;AAAA,UACA;AAAA,QACF,CAAC;AACD,kBAAU,OAAO;AACjB,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,CAAC,UAAU,QAAQ,MAAM,UAAU,WAAW,OAAO;AAAA,EACvD;AAEA,QAAM,QAAQA,aAAY,MAAM;AAC9B,aAAS;AAAA,MACP,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,OAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA,WAAW,MAAM,WAAW;AAAA,IAC5B,WAAW,MAAM,WAAW;AAAA,IAC5B,SAAS,MAAM,WAAW;AAAA,EAC5B;AACF;","names":["Fragment","jsx","jsxs","useState","useCallback"]}
@@ -0,0 +1,73 @@
1
+ import { R as RateLimitConfig } from './types-BmajlhNp.js';
2
+ export { A as APIResponse, C as ConfirmResult, E as EmailAdapter, a as EmailAdapterConfig, b as EmailTemplates, N as NewsletterConfig, c as NewsletterFormProps, d as NewsletterFormState, S as StorageAdapter, e as SubscribeInput, f as SubscribeRequest, g as SubscribeResult, h as Subscriber, i as SubscriptionStatus, U as UnsubscribeResult } from './types-BmajlhNp.js';
3
+ export { NewsletterHandlers, createNewsletterHandlers } from './server/index.js';
4
+ export { NewsletterBlock, NewsletterCard, NewsletterFooter, NewsletterForm, NewsletterInline, NewsletterModalContent, useNewsletter } from './components/index.js';
5
+ export { createMailchimpAdapter, createMailchimpStorageAdapter, createNodemailerAdapter, createResendAdapter } from './adapters/email/index.js';
6
+ export { createMemoryAdapter, createNoopAdapter, createPrismaAdapter, createSupabaseAdapter } from './adapters/storage/index.js';
7
+ import 'react/jsx-runtime';
8
+
9
+ /**
10
+ * Generate a secure random token for email confirmation
11
+ */
12
+ declare function generateToken(length?: number): string;
13
+ /**
14
+ * Generate a URL-safe token
15
+ */
16
+ declare function generateUrlSafeToken(length?: number): string;
17
+
18
+ /**
19
+ * Validate email format
20
+ * Uses a reasonable regex that catches most issues without being overly strict
21
+ */
22
+ declare function isValidEmail(email: string): boolean;
23
+ /**
24
+ * Normalize email address
25
+ * - Lowercase
26
+ * - Trim whitespace
27
+ * - Remove dots from Gmail local part (optional)
28
+ */
29
+ declare function normalizeEmail(email: string, options?: {
30
+ normalizeGmail?: boolean;
31
+ }): string;
32
+ declare function isDisposableEmail(email: string): boolean;
33
+ /**
34
+ * Sanitize string input to prevent XSS
35
+ */
36
+ declare function sanitizeString(input: string): string;
37
+
38
+ /**
39
+ * Simple in-memory rate limiter
40
+ *
41
+ * For production with multiple server instances, consider:
42
+ * - Redis-based rate limiting
43
+ * - Upstash Ratelimit
44
+ * - Arcjet
45
+ */
46
+ declare function createRateLimiter(config: RateLimitConfig): {
47
+ /**
48
+ * Check if request should be rate limited
49
+ * Returns true if request is allowed, false if rate limited
50
+ */
51
+ check(req: Request): Promise<{
52
+ allowed: boolean;
53
+ remaining: number;
54
+ resetAt: Date;
55
+ }>;
56
+ /**
57
+ * Get current limit status without incrementing
58
+ */
59
+ status(req: Request): Promise<{
60
+ remaining: number;
61
+ resetAt: Date | null;
62
+ }>;
63
+ /**
64
+ * Reset rate limit for an identifier
65
+ */
66
+ reset(req: Request): Promise<void>;
67
+ };
68
+ /**
69
+ * Extract client IP from request
70
+ */
71
+ declare function getClientIP(req: Request): string;
72
+
73
+ export { RateLimitConfig, createRateLimiter, generateToken, generateUrlSafeToken, getClientIP, isDisposableEmail, isValidEmail, normalizeEmail, sanitizeString };