@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/README.md ADDED
@@ -0,0 +1,505 @@
1
+ # @volchoklv/newsletter-kit
2
+
3
+ Drop-in newsletter subscription components and API handlers for Next.js with adapter support for email providers and storage backends.
4
+
5
+ ## Features
6
+
7
+ - 🎨 **shadcn/ui compatible** - Styled components that match your design system
8
+ - 🔌 **Adapter pattern** - Swap email providers and storage backends without changing code
9
+ - 🛡️ **Built-in protection** - Honeypot bot detection, rate limiting, email validation
10
+ - 📊 **Source tracking** - Track where subscribers come from
11
+ - ✅ **Double opt-in** - Optional email confirmation flow
12
+ - 🎯 **TypeScript first** - Full type safety
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @volchoklv/newsletter-kit
18
+ # or
19
+ pnpm add @volchoklv/newsletter-kit
20
+ # or
21
+ yarn add @volchoklv/newsletter-kit
22
+ ```
23
+
24
+ Install peer dependencies based on your choices:
25
+
26
+ ```bash
27
+ # For Resend
28
+ npm install resend
29
+
30
+ # For Nodemailer/SMTP
31
+ npm install nodemailer
32
+
33
+ # For Prisma storage
34
+ npm install @prisma/client
35
+
36
+ # For Supabase storage
37
+ npm install @supabase/supabase-js
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ### 1. Create the newsletter handler
43
+
44
+ ```ts
45
+ // lib/newsletter.ts
46
+ import { createNewsletterHandlers } from '@volchoklv/newsletter-kit/server';
47
+ import { createResendAdapter } from '@volchoklv/newsletter-kit/adapters/email';
48
+ import { createPrismaAdapter } from '@volchoklv/newsletter-kit/adapters/storage';
49
+ import { prisma } from '@/lib/prisma';
50
+
51
+ export const newsletter = createNewsletterHandlers({
52
+ emailAdapter: createResendAdapter({
53
+ apiKey: process.env.RESEND_API_KEY!,
54
+ from: 'newsletter@yourdomain.com',
55
+ adminEmail: 'you@yourdomain.com', // Get notified of new subscribers
56
+ }),
57
+ storageAdapter: createPrismaAdapter({ prisma }),
58
+ baseUrl: process.env.NEXT_PUBLIC_URL!,
59
+ doubleOptIn: true,
60
+ honeypotField: 'website',
61
+ rateLimit: {
62
+ max: 5,
63
+ windowSeconds: 60,
64
+ },
65
+ });
66
+ ```
67
+
68
+ ### 2. Create API routes
69
+
70
+ ```ts
71
+ // app/api/newsletter/subscribe/route.ts
72
+ import { newsletter } from '@/lib/newsletter';
73
+
74
+ export const POST = newsletter.subscribe;
75
+ ```
76
+
77
+ ```ts
78
+ // app/api/newsletter/confirm/route.ts
79
+ import { newsletter } from '@/lib/newsletter';
80
+
81
+ export const GET = newsletter.confirm;
82
+ ```
83
+
84
+ ```ts
85
+ // app/api/newsletter/unsubscribe/route.ts
86
+ import { newsletter } from '@/lib/newsletter';
87
+
88
+ export const POST = newsletter.unsubscribe;
89
+ export const GET = newsletter.unsubscribe;
90
+ ```
91
+
92
+ ### 3. Add the form component
93
+
94
+ ```tsx
95
+ // components/footer.tsx
96
+ import { NewsletterForm } from '@volchoklv/newsletter-kit/components';
97
+
98
+ export function Footer() {
99
+ return (
100
+ <footer>
101
+ <NewsletterForm
102
+ endpoint="/api/newsletter/subscribe"
103
+ source="footer"
104
+ placeholder="Enter your email"
105
+ buttonText="Subscribe"
106
+ />
107
+ </footer>
108
+ );
109
+ }
110
+ ```
111
+
112
+ ### 4. Add Prisma schema
113
+
114
+ **PostgreSQL / Neon / MySQL:**
115
+
116
+ ```prisma
117
+ model NewsletterSubscriber {
118
+ id String @id @default(cuid())
119
+ email String @unique
120
+ status String @default("pending")
121
+ token String? @unique
122
+ source String?
123
+ tags String[] @default([])
124
+ metadata Json?
125
+ consentIp String?
126
+ consentAt DateTime?
127
+ confirmedAt DateTime?
128
+ unsubscribedAt DateTime?
129
+ createdAt DateTime @default(now())
130
+ updatedAt DateTime @updatedAt
131
+
132
+ @@index([status])
133
+ @@index([source])
134
+ }
135
+ ```
136
+
137
+ **MongoDB:**
138
+
139
+ ```prisma
140
+ model NewsletterSubscriber {
141
+ id String @id @default(auto()) @map("_id") @db.ObjectId
142
+ email String @unique
143
+ status String @default("pending")
144
+ token String? @unique
145
+ source String?
146
+ tags String[] @default([])
147
+ metadata Json?
148
+ consentIp String?
149
+ consentAt DateTime?
150
+ confirmedAt DateTime?
151
+ unsubscribedAt DateTime?
152
+ createdAt DateTime @default(now())
153
+ updatedAt DateTime @updatedAt
154
+
155
+ @@index([status])
156
+ @@index([source])
157
+ }
158
+ ```
159
+
160
+ > **Note:** The adapter code works identically for both — Prisma handles the database differences.
161
+
162
+ ## Components
163
+
164
+ ### NewsletterForm
165
+
166
+ Basic form component with full customization:
167
+
168
+ ```tsx
169
+ <NewsletterForm
170
+ endpoint="/api/newsletter/subscribe"
171
+ source="homepage"
172
+ tags={['marketing', 'product-updates']}
173
+ placeholder="Enter your email"
174
+ buttonText="Subscribe"
175
+ loadingText="Subscribing..."
176
+ successMessage="Check your inbox to confirm!"
177
+ onSuccess={(email, message) => console.log('Subscribed:', email)}
178
+ onError={(error) => console.error('Error:', error)}
179
+ className="max-w-md"
180
+ inputClassName="border-2"
181
+ buttonClassName="bg-blue-600 hover:bg-blue-700"
182
+ />
183
+ ```
184
+
185
+ ### NewsletterBlock
186
+
187
+ Full-width section for landing pages:
188
+
189
+ ```tsx
190
+ <NewsletterBlock
191
+ endpoint="/api/newsletter/subscribe"
192
+ title="Stay in the loop"
193
+ description="Get weekly updates on the latest features and news."
194
+ source="landing-page"
195
+ />
196
+ ```
197
+
198
+ ### NewsletterCard
199
+
200
+ Card variant for sidebars:
201
+
202
+ ```tsx
203
+ <NewsletterCard
204
+ endpoint="/api/newsletter/subscribe"
205
+ title="Newsletter"
206
+ description="Don't miss out on updates."
207
+ source="sidebar"
208
+ />
209
+ ```
210
+
211
+ ### NewsletterFooter
212
+
213
+ Optimized for site footers:
214
+
215
+ ```tsx
216
+ <NewsletterFooter
217
+ endpoint="/api/newsletter/subscribe"
218
+ title="Newsletter"
219
+ description="Stay up to date."
220
+ source="footer"
221
+ privacyText="We respect your privacy."
222
+ privacyLink="/privacy"
223
+ />
224
+ ```
225
+
226
+ ### NewsletterModalContent
227
+
228
+ For use in dialogs/modals:
229
+
230
+ ```tsx
231
+ import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
232
+ import { NewsletterModalContent } from '@volchoklv/newsletter-kit/components';
233
+
234
+ export function NewsletterModal() {
235
+ const [open, setOpen] = useState(false);
236
+
237
+ return (
238
+ <Dialog open={open} onOpenChange={setOpen}>
239
+ <DialogTrigger asChild>
240
+ <Button>Subscribe</Button>
241
+ </DialogTrigger>
242
+ <DialogContent>
243
+ <NewsletterModalContent
244
+ endpoint="/api/newsletter/subscribe"
245
+ source="popup"
246
+ onSuccess={() => setTimeout(() => setOpen(false), 2000)}
247
+ />
248
+ </DialogContent>
249
+ </Dialog>
250
+ );
251
+ }
252
+ ```
253
+
254
+ ## Hook
255
+
256
+ For custom implementations:
257
+
258
+ ```tsx
259
+ import { useNewsletter } from '@volchoklv/newsletter-kit/components';
260
+
261
+ export function CustomForm() {
262
+ const { subscribe, isLoading, isSuccess, isError, message } = useNewsletter({
263
+ endpoint: '/api/newsletter/subscribe',
264
+ source: 'custom',
265
+ onSuccess: (email) => {
266
+ // Track in analytics
267
+ },
268
+ });
269
+
270
+ return (
271
+ <form onSubmit={(e) => {
272
+ e.preventDefault();
273
+ subscribe(e.currentTarget.email.value);
274
+ }}>
275
+ {/* Your custom UI */}
276
+ </form>
277
+ );
278
+ }
279
+ ```
280
+
281
+ ## Email Adapters
282
+
283
+ ### Resend
284
+
285
+ ```ts
286
+ import { createResendAdapter } from '@volchoklv/newsletter-kit/adapters/email';
287
+
288
+ const emailAdapter = createResendAdapter({
289
+ apiKey: process.env.RESEND_API_KEY!,
290
+ from: 'newsletter@yourdomain.com',
291
+ replyTo: 'hello@yourdomain.com',
292
+ adminEmail: 'you@yourdomain.com',
293
+ templates: {
294
+ confirmation: {
295
+ subject: 'Please confirm your subscription',
296
+ html: ({ confirmUrl, email }) => `
297
+ <h1>Confirm your subscription</h1>
298
+ <a href="${confirmUrl}">Click here to confirm</a>
299
+ `,
300
+ },
301
+ welcome: {
302
+ subject: 'Welcome aboard! 🎉',
303
+ html: ({ email }) => `
304
+ <h1>You're subscribed!</h1>
305
+ <p>Thanks for joining us.</p>
306
+ `,
307
+ },
308
+ },
309
+ });
310
+ ```
311
+
312
+ ### Nodemailer (SMTP)
313
+
314
+ ```ts
315
+ import { createNodemailerAdapter } from '@volchoklv/newsletter-kit/adapters/email';
316
+
317
+ const emailAdapter = createNodemailerAdapter({
318
+ smtp: {
319
+ host: 'smtp.example.com',
320
+ port: 587,
321
+ secure: false,
322
+ auth: {
323
+ user: process.env.SMTP_USER!,
324
+ pass: process.env.SMTP_PASS!,
325
+ },
326
+ },
327
+ from: 'newsletter@yourdomain.com',
328
+ });
329
+ ```
330
+
331
+ ### Mailchimp
332
+
333
+ ```ts
334
+ import { createMailchimpAdapter } from '@volchoklv/newsletter-kit/adapters/email';
335
+
336
+ const emailAdapter = createMailchimpAdapter({
337
+ apiKey: process.env.MAILCHIMP_API_KEY!,
338
+ server: 'us1',
339
+ listId: 'your-list-id',
340
+ from: 'newsletter@yourdomain.com',
341
+ useAsStorage: true, // Use Mailchimp as storage too
342
+ });
343
+ ```
344
+
345
+ ## Storage Adapters
346
+
347
+ ### Prisma
348
+
349
+ ```ts
350
+ import { createPrismaAdapter } from '@volchoklv/newsletter-kit/adapters/storage';
351
+ import { prisma } from '@/lib/prisma';
352
+
353
+ const storageAdapter = createPrismaAdapter({ prisma });
354
+ ```
355
+
356
+ ### Supabase
357
+
358
+ ```ts
359
+ import { createSupabaseAdapter } from '@volchoklv/newsletter-kit/adapters/storage';
360
+ import { createClient } from '@supabase/supabase-js';
361
+
362
+ const supabase = createClient(
363
+ process.env.SUPABASE_URL!,
364
+ process.env.SUPABASE_SERVICE_KEY!
365
+ );
366
+
367
+ const storageAdapter = createSupabaseAdapter({ supabase });
368
+ ```
369
+
370
+ ### In-Memory (Development/Testing)
371
+
372
+ ```ts
373
+ import { createMemoryAdapter } from '@volchoklv/newsletter-kit/adapters/storage';
374
+
375
+ const storageAdapter = createMemoryAdapter();
376
+ ```
377
+
378
+ ### No Storage (Email Only)
379
+
380
+ When using Mailchimp or similar that handles storage:
381
+
382
+ ```ts
383
+ import { createNoopAdapter } from '@volchoklv/newsletter-kit/adapters/storage';
384
+
385
+ const storageAdapter = createNoopAdapter();
386
+ ```
387
+
388
+ ## Configuration
389
+
390
+ Full configuration options:
391
+
392
+ ```ts
393
+ const newsletter = createNewsletterHandlers({
394
+ // Required
395
+ emailAdapter: createResendAdapter({ ... }),
396
+ baseUrl: 'https://yourdomain.com',
397
+
398
+ // Optional storage (defaults to in-memory)
399
+ storageAdapter: createPrismaAdapter({ prisma }),
400
+
401
+ // Double opt-in (default: true)
402
+ doubleOptIn: true,
403
+
404
+ // API paths (defaults shown)
405
+ confirmPath: '/api/newsletter/confirm',
406
+ unsubscribePath: '/api/newsletter/unsubscribe',
407
+
408
+ // Bot protection
409
+ honeypotField: 'website',
410
+
411
+ // Rate limiting
412
+ rateLimit: {
413
+ max: 5,
414
+ windowSeconds: 60,
415
+ },
416
+
417
+ // Custom email validation
418
+ validateEmail: async (email) => {
419
+ // Block disposable emails, etc.
420
+ return !email.includes('tempmail.com');
421
+ },
422
+
423
+ // Allowed sources for tracking
424
+ allowedSources: ['footer', 'sidebar', 'popup', 'landing-page'],
425
+
426
+ // Default tags for all subscribers
427
+ defaultTags: ['newsletter'],
428
+
429
+ // Callbacks
430
+ onSubscribe: async (subscriber) => {
431
+ // Track in analytics
432
+ },
433
+ onConfirm: async (subscriber) => {
434
+ // Send to CRM
435
+ },
436
+ onUnsubscribe: async (email) => {
437
+ // Update CRM
438
+ },
439
+ onError: async (error, context) => {
440
+ // Log to error tracking
441
+ },
442
+ });
443
+ ```
444
+
445
+ ## API Reference
446
+
447
+ ### Server Methods
448
+
449
+ ```ts
450
+ // Route handlers
451
+ newsletter.subscribe // POST handler
452
+ newsletter.confirm // GET handler
453
+ newsletter.unsubscribe // POST/GET handler
454
+
455
+ // Direct access
456
+ newsletter.handlers.subscribe(req)
457
+ newsletter.handlers.confirm(token)
458
+ newsletter.handlers.unsubscribe(email)
459
+
460
+ // Storage access
461
+ newsletter.storage.listSubscribers({ status: 'confirmed' })
462
+ newsletter.getSubscriber('email@example.com')
463
+ ```
464
+
465
+ ## Tailwind CSS
466
+
467
+ The components use Tailwind CSS with shadcn/ui-compatible class names. Make sure your `tailwind.config.js` includes:
468
+
469
+ ```js
470
+ module.exports = {
471
+ content: [
472
+ // ...
473
+ './node_modules/@volchoklv/newsletter-kit/**/*.{js,ts,jsx,tsx}',
474
+ ],
475
+ theme: {
476
+ extend: {
477
+ colors: {
478
+ border: 'hsl(var(--border))',
479
+ input: 'hsl(var(--input))',
480
+ ring: 'hsl(var(--ring))',
481
+ background: 'hsl(var(--background))',
482
+ foreground: 'hsl(var(--foreground))',
483
+ primary: {
484
+ DEFAULT: 'hsl(var(--primary))',
485
+ foreground: 'hsl(var(--primary-foreground))',
486
+ },
487
+ muted: {
488
+ DEFAULT: 'hsl(var(--muted))',
489
+ foreground: 'hsl(var(--muted-foreground))',
490
+ },
491
+ card: {
492
+ DEFAULT: 'hsl(var(--card))',
493
+ foreground: 'hsl(var(--card-foreground))',
494
+ },
495
+ },
496
+ },
497
+ },
498
+ };
499
+ ```
500
+
501
+ Or use the `className` props to apply your own styles.
502
+
503
+ ## License
504
+
505
+ MIT
@@ -0,0 +1,119 @@
1
+ import { a as EmailAdapterConfig, E as EmailAdapter } from '../../types-BmajlhNp.js';
2
+ export { b as EmailTemplates } from '../../types-BmajlhNp.js';
3
+
4
+ interface ResendAdapterConfig extends EmailAdapterConfig {
5
+ apiKey: string;
6
+ }
7
+ /**
8
+ * Email adapter for Resend (https://resend.com)
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { createResendAdapter } from '@volchok/newsletter-kit/adapters/email';
13
+ *
14
+ * const emailAdapter = createResendAdapter({
15
+ * apiKey: process.env.RESEND_API_KEY!,
16
+ * from: 'newsletter@yourdomain.com',
17
+ * adminEmail: 'you@yourdomain.com', // optional
18
+ * });
19
+ * ```
20
+ */
21
+ declare function createResendAdapter(config: ResendAdapterConfig): EmailAdapter;
22
+
23
+ interface SMTPConfig {
24
+ host: string;
25
+ port: number;
26
+ secure?: boolean;
27
+ auth?: {
28
+ user: string;
29
+ pass: string;
30
+ };
31
+ }
32
+ interface NodemailerAdapterConfig extends EmailAdapterConfig {
33
+ smtp: SMTPConfig;
34
+ }
35
+ /**
36
+ * Email adapter for Nodemailer/SMTP
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { createNodemailerAdapter } from '@volchok/newsletter-kit/adapters/email';
41
+ *
42
+ * const emailAdapter = createNodemailerAdapter({
43
+ * smtp: {
44
+ * host: 'smtp.example.com',
45
+ * port: 587,
46
+ * secure: false,
47
+ * auth: {
48
+ * user: process.env.SMTP_USER!,
49
+ * pass: process.env.SMTP_PASS!,
50
+ * },
51
+ * },
52
+ * from: 'newsletter@yourdomain.com',
53
+ * adminEmail: 'you@yourdomain.com', // optional
54
+ * });
55
+ * ```
56
+ */
57
+ declare function createNodemailerAdapter(config: NodemailerAdapterConfig): EmailAdapter;
58
+
59
+ interface MailchimpAdapterConfig extends EmailAdapterConfig {
60
+ apiKey: string;
61
+ server: string;
62
+ listId: string;
63
+ /** If true, use Mailchimp for storage too (adds to list on subscribe) */
64
+ useAsStorage?: boolean;
65
+ }
66
+ /**
67
+ * Email adapter for Mailchimp
68
+ *
69
+ * Note: Mailchimp works differently from other adapters. It manages its own
70
+ * subscriber list, so this adapter can optionally act as both email AND storage.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * import { createMailchimpAdapter } from '@volchok/newsletter-kit/adapters/email';
75
+ *
76
+ * const emailAdapter = createMailchimpAdapter({
77
+ * apiKey: process.env.MAILCHIMP_API_KEY!,
78
+ * server: 'us1',
79
+ * listId: 'your-list-id',
80
+ * from: 'newsletter@yourdomain.com',
81
+ * useAsStorage: true, // Use Mailchimp's list as storage
82
+ * });
83
+ * ```
84
+ */
85
+ declare function createMailchimpAdapter(config: MailchimpAdapterConfig): EmailAdapter;
86
+ /**
87
+ * Create a combined Mailchimp adapter that handles both email and storage
88
+ *
89
+ * This is useful when you want Mailchimp to be your single source of truth
90
+ * for subscribers.
91
+ */
92
+ declare function createMailchimpStorageAdapter(config: MailchimpAdapterConfig): {
93
+ createSubscriber(input: {
94
+ email: string;
95
+ source?: string;
96
+ tags?: string[];
97
+ }): Promise<{
98
+ id: string;
99
+ email: string;
100
+ status: "pending";
101
+ source: string | undefined;
102
+ tags: string[] | undefined;
103
+ createdAt: Date;
104
+ updatedAt: Date;
105
+ }>;
106
+ getSubscriberByEmail(email: string): Promise<{
107
+ id: string;
108
+ email: string;
109
+ status: string;
110
+ tags: string[] | undefined;
111
+ createdAt: Date;
112
+ updatedAt: Date;
113
+ } | null>;
114
+ getSubscriberByToken(): Promise<null>;
115
+ confirmSubscriber(_token: string): Promise<null>;
116
+ unsubscribe(email: string): Promise<boolean>;
117
+ };
118
+
119
+ export { EmailAdapter, EmailAdapterConfig, createMailchimpAdapter, createMailchimpStorageAdapter, createNodemailerAdapter, createResendAdapter };