@djangocfg/ext-newsletter 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 +140 -0
- package/dist/chunk-LQLPNWHR.js +2075 -0
- package/dist/hooks.cjs +2539 -0
- package/dist/hooks.d.cts +267 -0
- package/dist/hooks.d.ts +267 -0
- package/dist/hooks.js +370 -0
- package/dist/index.cjs +2153 -0
- package/dist/index.d.cts +2084 -0
- package/dist/index.d.ts +2084 -0
- package/dist/index.js +1 -0
- package/package.json +80 -0
- package/src/api/generated/ext_newsletter/_utils/fetchers/ext_newsletter__newsletter.ts +210 -0
- package/src/api/generated/ext_newsletter/_utils/fetchers/ext_newsletter__newsletter__bulk_email.ts +93 -0
- package/src/api/generated/ext_newsletter/_utils/fetchers/ext_newsletter__newsletter__campaigns.ts +338 -0
- package/src/api/generated/ext_newsletter/_utils/fetchers/ext_newsletter__newsletter__logs.ts +92 -0
- package/src/api/generated/ext_newsletter/_utils/fetchers/ext_newsletter__newsletter__newsletters.ts +150 -0
- package/src/api/generated/ext_newsletter/_utils/fetchers/ext_newsletter__newsletter__subscriptions.ts +210 -0
- package/src/api/generated/ext_newsletter/_utils/fetchers/ext_newsletter__newsletter__testing.ts +93 -0
- package/src/api/generated/ext_newsletter/_utils/fetchers/index.ts +34 -0
- package/src/api/generated/ext_newsletter/_utils/hooks/ext_newsletter__newsletter.ts +81 -0
- package/src/api/generated/ext_newsletter/_utils/hooks/ext_newsletter__newsletter__bulk_email.ts +42 -0
- package/src/api/generated/ext_newsletter/_utils/hooks/ext_newsletter__newsletter__campaigns.ts +130 -0
- package/src/api/generated/ext_newsletter/_utils/hooks/ext_newsletter__newsletter__logs.ts +37 -0
- package/src/api/generated/ext_newsletter/_utils/hooks/ext_newsletter__newsletter__newsletters.ts +52 -0
- package/src/api/generated/ext_newsletter/_utils/hooks/ext_newsletter__newsletter__subscriptions.ts +78 -0
- package/src/api/generated/ext_newsletter/_utils/hooks/ext_newsletter__newsletter__testing.ts +42 -0
- package/src/api/generated/ext_newsletter/_utils/hooks/index.ts +34 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/BulkEmailRequest.schema.ts +26 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/BulkEmailResponse.schema.ts +23 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/EmailLog.schema.ts +31 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/ErrorResponse.schema.ts +20 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/Newsletter.schema.ts +26 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/NewsletterCampaign.schema.ts +33 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/NewsletterCampaignRequest.schema.ts +26 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/NewsletterSubscription.schema.ts +27 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/PaginatedEmailLogList.schema.ts +24 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/PaginatedNewsletterCampaignList.schema.ts +24 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/PaginatedNewsletterList.schema.ts +24 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/PaginatedNewsletterSubscriptionList.schema.ts +24 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/PatchedNewsletterCampaignRequest.schema.ts +26 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/PatchedUnsubscribeRequest.schema.ts +19 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/SendCampaignRequest.schema.ts +19 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/SendCampaignResponse.schema.ts +22 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/SubscribeRequest.schema.ts +20 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/SubscribeResponse.schema.ts +21 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/SuccessResponse.schema.ts +20 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/TestEmailRequest.schema.ts +21 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/Unsubscribe.schema.ts +19 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/UnsubscribeRequest.schema.ts +19 -0
- package/src/api/generated/ext_newsletter/_utils/schemas/index.ts +40 -0
- package/src/api/generated/ext_newsletter/api-instance.ts +131 -0
- package/src/api/generated/ext_newsletter/client.ts +319 -0
- package/src/api/generated/ext_newsletter/enums.ts +24 -0
- package/src/api/generated/ext_newsletter/errors.ts +116 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter/client.ts +38 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter/index.ts +2 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter/models.ts +71 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__bulk_email/client.ts +24 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__bulk_email/index.ts +2 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__bulk_email/models.ts +29 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__campaigns/client.ts +85 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__campaigns/index.ts +2 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__campaigns/models.ts +100 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__logs/client.ts +35 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__logs/index.ts +2 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__logs/models.ts +51 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__newsletters/client.ts +45 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__newsletters/index.ts +2 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__newsletters/models.ts +42 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__subscriptions/client.ts +55 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__subscriptions/index.ts +2 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__subscriptions/models.ts +92 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__testing/client.ts +24 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__testing/index.ts +2 -0
- package/src/api/generated/ext_newsletter/ext_newsletter__newsletter__testing/models.ts +24 -0
- package/src/api/generated/ext_newsletter/http.ts +103 -0
- package/src/api/generated/ext_newsletter/index.ts +315 -0
- package/src/api/generated/ext_newsletter/logger.ts +259 -0
- package/src/api/generated/ext_newsletter/retry.ts +175 -0
- package/src/api/generated/ext_newsletter/schema.json +1739 -0
- package/src/api/generated/ext_newsletter/storage.ts +161 -0
- package/src/api/generated/ext_newsletter/validation-events.ts +133 -0
- package/src/api/index.ts +9 -0
- package/src/components/Hero/index.tsx +160 -0
- package/src/components/Hero/types.ts +37 -0
- package/src/config.ts +20 -0
- package/src/contexts/newsletter/NewsletterContext.tsx +264 -0
- package/src/contexts/newsletter/types.ts +32 -0
- package/src/hooks/index.ts +21 -0
- package/src/index.ts +14 -0
- package/src/utils/logger.ts +9 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage adapters for cross-platform token storage.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - LocalStorage (browser)
|
|
6
|
+
* - Cookies (SSR/browser)
|
|
7
|
+
* - Memory (Node.js/Electron/testing)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { APILogger } from './logger';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Storage adapter interface for cross-platform token storage.
|
|
14
|
+
*/
|
|
15
|
+
export interface StorageAdapter {
|
|
16
|
+
getItem(key: string): string | null;
|
|
17
|
+
setItem(key: string, value: string): void;
|
|
18
|
+
removeItem(key: string): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* LocalStorage adapter with safe try-catch for browser environments.
|
|
23
|
+
* Works in modern browsers with localStorage support.
|
|
24
|
+
*
|
|
25
|
+
* Note: This adapter uses window.localStorage and should only be used in browser/client environments.
|
|
26
|
+
* For server-side usage, use MemoryStorageAdapter or CookieStorageAdapter instead.
|
|
27
|
+
*/
|
|
28
|
+
export class LocalStorageAdapter implements StorageAdapter {
|
|
29
|
+
private logger?: APILogger;
|
|
30
|
+
|
|
31
|
+
constructor(logger?: APILogger) {
|
|
32
|
+
this.logger = logger;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getItem(key: string): string | null {
|
|
36
|
+
try {
|
|
37
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
38
|
+
const value = localStorage.getItem(key);
|
|
39
|
+
this.logger?.debug(`LocalStorage.getItem("${key}"): ${value ? 'found' : 'not found'}`);
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
this.logger?.warn('LocalStorage not available: window.localStorage is undefined');
|
|
43
|
+
} catch (error) {
|
|
44
|
+
this.logger?.error('LocalStorage.getItem failed:', error);
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setItem(key: string, value: string): void {
|
|
50
|
+
try {
|
|
51
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
52
|
+
localStorage.setItem(key, value);
|
|
53
|
+
this.logger?.debug(`LocalStorage.setItem("${key}"): success`);
|
|
54
|
+
} else {
|
|
55
|
+
this.logger?.warn('LocalStorage not available: window.localStorage is undefined');
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.logger?.error('LocalStorage.setItem failed:', error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
removeItem(key: string): void {
|
|
63
|
+
try {
|
|
64
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
65
|
+
localStorage.removeItem(key);
|
|
66
|
+
this.logger?.debug(`LocalStorage.removeItem("${key}"): success`);
|
|
67
|
+
} else {
|
|
68
|
+
this.logger?.warn('LocalStorage not available: window.localStorage is undefined');
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.logger?.error('LocalStorage.removeItem failed:', error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Cookie-based storage adapter for SSR and browser environments.
|
|
78
|
+
* Useful for Next.js, Nuxt.js, and other SSR frameworks.
|
|
79
|
+
*/
|
|
80
|
+
export class CookieStorageAdapter implements StorageAdapter {
|
|
81
|
+
private logger?: APILogger;
|
|
82
|
+
|
|
83
|
+
constructor(logger?: APILogger) {
|
|
84
|
+
this.logger = logger;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getItem(key: string): string | null {
|
|
88
|
+
try {
|
|
89
|
+
if (typeof document === 'undefined') {
|
|
90
|
+
this.logger?.warn('Cookies not available: document is undefined (SSR context?)');
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const value = `; ${document.cookie}`;
|
|
94
|
+
const parts = value.split(`; ${key}=`);
|
|
95
|
+
if (parts.length === 2) {
|
|
96
|
+
const result = parts.pop()?.split(';').shift() || null;
|
|
97
|
+
this.logger?.debug(`CookieStorage.getItem("${key}"): ${result ? 'found' : 'not found'}`);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
this.logger?.debug(`CookieStorage.getItem("${key}"): not found`);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
this.logger?.error('CookieStorage.getItem failed:', error);
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setItem(key: string, value: string): void {
|
|
108
|
+
try {
|
|
109
|
+
if (typeof document !== 'undefined') {
|
|
110
|
+
document.cookie = `${key}=${value}; path=/; max-age=31536000`;
|
|
111
|
+
this.logger?.debug(`CookieStorage.setItem("${key}"): success`);
|
|
112
|
+
} else {
|
|
113
|
+
this.logger?.warn('Cookies not available: document is undefined (SSR context?)');
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
this.logger?.error('CookieStorage.setItem failed:', error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
removeItem(key: string): void {
|
|
121
|
+
try {
|
|
122
|
+
if (typeof document !== 'undefined') {
|
|
123
|
+
document.cookie = `${key}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
124
|
+
this.logger?.debug(`CookieStorage.removeItem("${key}"): success`);
|
|
125
|
+
} else {
|
|
126
|
+
this.logger?.warn('Cookies not available: document is undefined (SSR context?)');
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
this.logger?.error('CookieStorage.removeItem failed:', error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* In-memory storage adapter for Node.js, Electron, and testing environments.
|
|
136
|
+
* Data is stored in RAM and cleared when process exits.
|
|
137
|
+
*/
|
|
138
|
+
export class MemoryStorageAdapter implements StorageAdapter {
|
|
139
|
+
private storage: Map<string, string> = new Map();
|
|
140
|
+
private logger?: APILogger;
|
|
141
|
+
|
|
142
|
+
constructor(logger?: APILogger) {
|
|
143
|
+
this.logger = logger;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getItem(key: string): string | null {
|
|
147
|
+
const value = this.storage.get(key) || null;
|
|
148
|
+
this.logger?.debug(`MemoryStorage.getItem("${key}"): ${value ? 'found' : 'not found'}`);
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setItem(key: string, value: string): void {
|
|
153
|
+
this.storage.set(key, value);
|
|
154
|
+
this.logger?.debug(`MemoryStorage.setItem("${key}"): success`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
removeItem(key: string): void {
|
|
158
|
+
this.storage.delete(key);
|
|
159
|
+
this.logger?.debug(`MemoryStorage.removeItem("${key}"): success`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod Validation Events - Browser CustomEvent integration
|
|
3
|
+
*
|
|
4
|
+
* Dispatches browser CustomEvents when Zod validation fails, allowing
|
|
5
|
+
* React/frontend apps to listen and handle validation errors globally.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // In your React app
|
|
10
|
+
* window.addEventListener('zod-validation-error', (event) => {
|
|
11
|
+
* const { operation, path, method, error, response } = event.detail;
|
|
12
|
+
* console.error(`Validation failed for ${method} ${path}`, error);
|
|
13
|
+
* // Show toast notification, log to Sentry, etc.
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ZodError } from 'zod'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validation error event detail
|
|
22
|
+
*/
|
|
23
|
+
export interface ValidationErrorDetail {
|
|
24
|
+
/** Operation/function name that failed validation */
|
|
25
|
+
operation: string
|
|
26
|
+
/** API endpoint path */
|
|
27
|
+
path: string
|
|
28
|
+
/** HTTP method */
|
|
29
|
+
method: string
|
|
30
|
+
/** Zod validation error */
|
|
31
|
+
error: ZodError
|
|
32
|
+
/** Raw response data that failed validation */
|
|
33
|
+
response: any
|
|
34
|
+
/** Timestamp of the error */
|
|
35
|
+
timestamp: Date
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Custom event type for Zod validation errors
|
|
40
|
+
*/
|
|
41
|
+
export type ValidationErrorEvent = CustomEvent<ValidationErrorDetail>
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Dispatch a Zod validation error event.
|
|
45
|
+
*
|
|
46
|
+
* Only dispatches in browser environment (when window is defined).
|
|
47
|
+
* Safe to call in Node.js/SSR - will be a no-op.
|
|
48
|
+
*
|
|
49
|
+
* @param detail - Validation error details
|
|
50
|
+
*/
|
|
51
|
+
export function dispatchValidationError(detail: ValidationErrorDetail): void {
|
|
52
|
+
// Check if running in browser
|
|
53
|
+
if (typeof window === 'undefined') {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const event = new CustomEvent<ValidationErrorDetail>('zod-validation-error', {
|
|
59
|
+
detail,
|
|
60
|
+
bubbles: true,
|
|
61
|
+
cancelable: false,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
window.dispatchEvent(event)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Silently fail - validation event dispatch should never crash the app
|
|
67
|
+
console.warn('Failed to dispatch validation error event:', error)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Add a global listener for Zod validation errors.
|
|
73
|
+
*
|
|
74
|
+
* @param callback - Function to call when validation error occurs
|
|
75
|
+
* @returns Cleanup function to remove the listener
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const cleanup = onValidationError(({ operation, error }) => {
|
|
80
|
+
* toast.error(`Validation failed in ${operation}`);
|
|
81
|
+
* logToSentry(error);
|
|
82
|
+
* });
|
|
83
|
+
*
|
|
84
|
+
* // Later, remove listener
|
|
85
|
+
* cleanup();
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function onValidationError(
|
|
89
|
+
callback: (detail: ValidationErrorDetail) => void
|
|
90
|
+
): () => void {
|
|
91
|
+
if (typeof window === 'undefined') {
|
|
92
|
+
// Return no-op cleanup function for SSR
|
|
93
|
+
return () => {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const handler = (event: Event) => {
|
|
97
|
+
if (event instanceof CustomEvent) {
|
|
98
|
+
callback(event.detail)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
window.addEventListener('zod-validation-error', handler)
|
|
103
|
+
|
|
104
|
+
// Return cleanup function
|
|
105
|
+
return () => {
|
|
106
|
+
window.removeEventListener('zod-validation-error', handler)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Format Zod error for logging/display.
|
|
112
|
+
*
|
|
113
|
+
* @param error - Zod validation error
|
|
114
|
+
* @returns Formatted error message
|
|
115
|
+
*/
|
|
116
|
+
export function formatZodError(error: ZodError): string {
|
|
117
|
+
const issues = error.issues.map((issue, index) => {
|
|
118
|
+
const path = issue.path.join('.') || 'root'
|
|
119
|
+
const parts = [`${index + 1}. ${path}: ${issue.message}`]
|
|
120
|
+
|
|
121
|
+
if ('expected' in issue && issue.expected) {
|
|
122
|
+
parts.push(` Expected: ${issue.expected}`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if ('received' in issue && issue.received) {
|
|
126
|
+
parts.push(` Received: ${issue.received}`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return parts.join('\n')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
return issues.join('\n')
|
|
133
|
+
}
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter Extension API
|
|
3
|
+
*
|
|
4
|
+
* Pre-configured API instance with shared authentication
|
|
5
|
+
*/
|
|
6
|
+
import { API } from './generated/ext_newsletter';
|
|
7
|
+
import { createExtensionAPI } from '@djangocfg/ext-base/api';
|
|
8
|
+
|
|
9
|
+
export const apiNewsletter = createExtensionAPI(API);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hero Component with Newsletter Subscription
|
|
3
|
+
* Landing page hero section with integrated email subscription form
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useState } from 'react';
|
|
9
|
+
import { Button, Input } from '@djangocfg/ui-nextjs';
|
|
10
|
+
import { Mail, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react';
|
|
11
|
+
import { newsletterLogger } from '../../utils/logger';
|
|
12
|
+
import type { HeroProps } from './types';
|
|
13
|
+
|
|
14
|
+
export function Hero({
|
|
15
|
+
title,
|
|
16
|
+
description,
|
|
17
|
+
primaryAction,
|
|
18
|
+
secondaryAction,
|
|
19
|
+
showNewsletter = true,
|
|
20
|
+
newsletterPlaceholder = 'Enter your email',
|
|
21
|
+
newsletterButtonText = 'Subscribe',
|
|
22
|
+
onNewsletterSubmit,
|
|
23
|
+
className = '',
|
|
24
|
+
}: HeroProps) {
|
|
25
|
+
const [email, setEmail] = useState('');
|
|
26
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
27
|
+
const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
|
28
|
+
const [message, setMessage] = useState('');
|
|
29
|
+
|
|
30
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
|
|
33
|
+
if (!email || !onNewsletterSubmit) return;
|
|
34
|
+
|
|
35
|
+
setIsLoading(true);
|
|
36
|
+
setStatus('idle');
|
|
37
|
+
setMessage('');
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const result = await onNewsletterSubmit(email);
|
|
41
|
+
setStatus('success');
|
|
42
|
+
setMessage((result && 'message' in result ? result.message : undefined) || 'Successfully subscribed!');
|
|
43
|
+
setEmail('');
|
|
44
|
+
newsletterLogger.success('Newsletter subscription successful:', email);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
setStatus('error');
|
|
47
|
+
setMessage(error instanceof Error ? error.message : 'Subscription failed. Please try again.');
|
|
48
|
+
newsletterLogger.error('Newsletter subscription failed:', error);
|
|
49
|
+
} finally {
|
|
50
|
+
setIsLoading(false);
|
|
51
|
+
|
|
52
|
+
// Reset status after 5 seconds
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
setStatus('idle');
|
|
55
|
+
setMessage('');
|
|
56
|
+
}, 5000);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<section className={`relative py-16 sm:py-20 md:py-24 lg:py-32 ${className}`}>
|
|
62
|
+
<div className="container max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
63
|
+
<div className="max-w-3xl mx-auto text-center">
|
|
64
|
+
{/* Title */}
|
|
65
|
+
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold tracking-tight text-foreground mb-6">
|
|
66
|
+
{title}
|
|
67
|
+
</h1>
|
|
68
|
+
|
|
69
|
+
{/* Description */}
|
|
70
|
+
{description && (
|
|
71
|
+
<p className="text-lg sm:text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
|
|
72
|
+
{description}
|
|
73
|
+
</p>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{/* Actions */}
|
|
77
|
+
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-12">
|
|
78
|
+
{primaryAction && (
|
|
79
|
+
<Button
|
|
80
|
+
size="lg"
|
|
81
|
+
onClick={primaryAction.onClick}
|
|
82
|
+
className="w-full sm:w-auto"
|
|
83
|
+
>
|
|
84
|
+
{primaryAction.label}
|
|
85
|
+
</Button>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{secondaryAction && (
|
|
89
|
+
<Button
|
|
90
|
+
size="lg"
|
|
91
|
+
variant="outline"
|
|
92
|
+
onClick={secondaryAction.onClick}
|
|
93
|
+
className="w-full sm:w-auto"
|
|
94
|
+
>
|
|
95
|
+
{secondaryAction.label}
|
|
96
|
+
</Button>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Newsletter Subscription */}
|
|
101
|
+
{showNewsletter && onNewsletterSubmit && (
|
|
102
|
+
<div className="max-w-md mx-auto">
|
|
103
|
+
<form onSubmit={handleSubmit} className="space-y-3">
|
|
104
|
+
<div className="flex flex-col sm:flex-row gap-2">
|
|
105
|
+
<div className="relative flex-1">
|
|
106
|
+
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
|
107
|
+
<Input
|
|
108
|
+
type="email"
|
|
109
|
+
placeholder={newsletterPlaceholder}
|
|
110
|
+
value={email}
|
|
111
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
112
|
+
disabled={isLoading}
|
|
113
|
+
required
|
|
114
|
+
className="pl-10"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
<Button
|
|
118
|
+
type="submit"
|
|
119
|
+
disabled={isLoading || !email}
|
|
120
|
+
className="w-full sm:w-auto"
|
|
121
|
+
>
|
|
122
|
+
{isLoading ? (
|
|
123
|
+
<>
|
|
124
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
125
|
+
Subscribing...
|
|
126
|
+
</>
|
|
127
|
+
) : (
|
|
128
|
+
newsletterButtonText
|
|
129
|
+
)}
|
|
130
|
+
</Button>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Status Messages */}
|
|
134
|
+
{status === 'success' && (
|
|
135
|
+
<div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
|
|
136
|
+
<CheckCircle2 className="h-4 w-4" />
|
|
137
|
+
<span>{message}</span>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{status === 'error' && (
|
|
142
|
+
<div className="flex items-center gap-2 text-sm text-red-600 dark:text-red-400">
|
|
143
|
+
<AlertCircle className="h-4 w-4" />
|
|
144
|
+
<span>{message}</span>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</form>
|
|
148
|
+
|
|
149
|
+
<p className="text-xs text-muted-foreground mt-3">
|
|
150
|
+
By subscribing, you agree to our Privacy Policy and consent to receive updates.
|
|
151
|
+
</p>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</section>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default Hero;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hero Component Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface HeroAction {
|
|
6
|
+
label: string;
|
|
7
|
+
onClick: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface HeroProps {
|
|
11
|
+
/** Main heading text */
|
|
12
|
+
title: string;
|
|
13
|
+
|
|
14
|
+
/** Supporting description text */
|
|
15
|
+
description?: string;
|
|
16
|
+
|
|
17
|
+
/** Primary call-to-action button */
|
|
18
|
+
primaryAction?: HeroAction;
|
|
19
|
+
|
|
20
|
+
/** Secondary call-to-action button */
|
|
21
|
+
secondaryAction?: HeroAction;
|
|
22
|
+
|
|
23
|
+
/** Show newsletter subscription form */
|
|
24
|
+
showNewsletter?: boolean;
|
|
25
|
+
|
|
26
|
+
/** Email input placeholder */
|
|
27
|
+
newsletterPlaceholder?: string;
|
|
28
|
+
|
|
29
|
+
/** Subscribe button text */
|
|
30
|
+
newsletterButtonText?: string;
|
|
31
|
+
|
|
32
|
+
/** Newsletter submission handler */
|
|
33
|
+
onNewsletterSubmit?: (email: string) => Promise<{ message?: string } | void>;
|
|
34
|
+
|
|
35
|
+
/** Additional CSS classes */
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newsletter extension configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionMetadata } from '@djangocfg/ext-base';
|
|
6
|
+
|
|
7
|
+
export const extensionConfig: ExtensionMetadata = {
|
|
8
|
+
name: 'newsletter',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
author: 'DjangoCFG',
|
|
11
|
+
displayName: 'Newsletter',
|
|
12
|
+
description: 'Newsletter subscription and campaign management',
|
|
13
|
+
icon: '📧',
|
|
14
|
+
license: 'MIT',
|
|
15
|
+
githubUrl: 'https://github.com/markolofsen/django-cfg',
|
|
16
|
+
homepage: 'https://djangocfg.com',
|
|
17
|
+
keywords: ['newsletter', 'email', 'subscription', 'campaign', 'marketing'],
|
|
18
|
+
dependencies: [],
|
|
19
|
+
minVersion: '2.0.0',
|
|
20
|
+
} as const;
|