ayezee-astro-cms 1.0.0 → 1.2.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,54 @@
1
+ /**
2
+ * Cloudinary URL helpers for optimized image delivery
3
+ *
4
+ * Upload presets store originals at full quality with NO transformations.
5
+ * Transformations are applied dynamically via URL at delivery time.
6
+ *
7
+ * Presets in Cloudinary dashboard:
8
+ * - ayezee_gallery: For gallery/fan-wall images → folder: gallery-images
9
+ * - ayezee_optimized: For project logos → folder: project-logos
10
+ */
11
+ type CloudinaryTransformOptions = {
12
+ width?: number;
13
+ height?: number;
14
+ quality?: number | "auto" | "auto:best" | "auto:good" | "auto:eco" | "auto:low";
15
+ format?: "auto" | "webp" | "avif" | "jpg" | "png";
16
+ crop?: "fill" | "fit" | "limit" | "scale" | "thumb" | "pad";
17
+ gravity?: "auto" | "face" | "center" | "north" | "south" | "east" | "west";
18
+ dpr?: number | "auto";
19
+ aspectRatio?: string;
20
+ };
21
+ /**
22
+ * Build a Cloudinary URL with transformations
23
+ */
24
+ export declare function buildCloudinaryUrl(url: string, options?: CloudinaryTransformOptions): string;
25
+ /**
26
+ * Generate srcset for responsive images
27
+ */
28
+ export declare function buildCloudinarySrcSet(url: string, widths: number[], quality?: CloudinaryTransformOptions["quality"]): string;
29
+ /**
30
+ * Preset configurations for common use cases
31
+ */
32
+ export declare const CLOUDINARY_PRESETS: {
33
+ readonly logo: {
34
+ readonly default: (url: string) => string;
35
+ readonly thumb: (url: string) => string;
36
+ readonly large: (url: string) => string;
37
+ };
38
+ readonly gallery: {
39
+ readonly src: (url: string) => string;
40
+ readonly srcset: (url: string) => string;
41
+ readonly hero: (url: string) => string;
42
+ readonly thumb: (url: string) => string;
43
+ };
44
+ readonly avatar: (url: string) => string;
45
+ };
46
+ /**
47
+ * Helper to check if a URL is from Cloudinary
48
+ */
49
+ export declare function isCloudinaryUrl(url: string): boolean;
50
+ /**
51
+ * Extract public ID from Cloudinary URL
52
+ */
53
+ export declare function getCloudinaryPublicId(url: string): string | null;
54
+ export {};
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Cloudinary URL helpers for optimized image delivery
3
+ *
4
+ * Upload presets store originals at full quality with NO transformations.
5
+ * Transformations are applied dynamically via URL at delivery time.
6
+ *
7
+ * Presets in Cloudinary dashboard:
8
+ * - ayezee_gallery: For gallery/fan-wall images → folder: gallery-images
9
+ * - ayezee_optimized: For project logos → folder: project-logos
10
+ */
11
+ /**
12
+ * Build a Cloudinary URL with transformations
13
+ */
14
+ export function buildCloudinaryUrl(url, options = {}) {
15
+ if (!url || !url.includes("cloudinary.com"))
16
+ return url;
17
+ const uploadIndex = url.indexOf("/upload/");
18
+ if (uploadIndex === -1)
19
+ return url;
20
+ const baseUrl = url.substring(0, uploadIndex + 8); // includes '/upload/'
21
+ const imagePath = url.substring(uploadIndex + 8);
22
+ const transforms = [];
23
+ // Format (most important for file size + quality)
24
+ if (options.format !== undefined) {
25
+ transforms.push(`f_${options.format}`);
26
+ }
27
+ else {
28
+ transforms.push("f_auto"); // Auto-select WebP/AVIF for modern browsers
29
+ }
30
+ // Quality
31
+ if (options.quality !== undefined) {
32
+ transforms.push(`q_${options.quality}`);
33
+ }
34
+ // Device pixel ratio (retina support)
35
+ if (options.dpr !== undefined) {
36
+ transforms.push(`dpr_${options.dpr}`);
37
+ }
38
+ else {
39
+ transforms.push("dpr_auto"); // Auto-detect retina displays
40
+ }
41
+ // Dimensions
42
+ if (options.width) {
43
+ transforms.push(`w_${options.width}`);
44
+ }
45
+ if (options.height) {
46
+ transforms.push(`h_${options.height}`);
47
+ }
48
+ if (options.aspectRatio) {
49
+ transforms.push(`ar_${options.aspectRatio}`);
50
+ }
51
+ // Cropping
52
+ if (options.crop) {
53
+ transforms.push(`c_${options.crop}`);
54
+ }
55
+ else if (options.width || options.height) {
56
+ transforms.push("c_limit"); // Don't upscale beyond original
57
+ }
58
+ // Gravity (for cropping)
59
+ if (options.gravity) {
60
+ transforms.push(`g_${options.gravity}`);
61
+ }
62
+ return `${baseUrl}${transforms.join(",")}/${imagePath}`;
63
+ }
64
+ /**
65
+ * Generate srcset for responsive images
66
+ */
67
+ export function buildCloudinarySrcSet(url, widths, quality = "auto:good") {
68
+ return widths
69
+ .map((width) => `${buildCloudinaryUrl(url, { width, quality })} ${width}w`)
70
+ .join(", ");
71
+ }
72
+ /**
73
+ * Preset configurations for common use cases
74
+ */
75
+ export const CLOUDINARY_PRESETS = {
76
+ // Project logos
77
+ logo: {
78
+ // Standard logo display
79
+ default: (url) => buildCloudinaryUrl(url, {
80
+ width: 400,
81
+ quality: "auto:good",
82
+ crop: "limit",
83
+ }),
84
+ // Small thumbnail (for lists)
85
+ thumb: (url) => buildCloudinaryUrl(url, {
86
+ width: 100,
87
+ height: 100,
88
+ quality: "auto:good",
89
+ crop: "fill",
90
+ gravity: "center",
91
+ }),
92
+ // Large (for hero sections)
93
+ large: (url) => buildCloudinaryUrl(url, {
94
+ width: 800,
95
+ quality: "auto:best",
96
+ crop: "limit",
97
+ }),
98
+ },
99
+ // Gallery images - high quality, responsive
100
+ gallery: {
101
+ // Default src (mobile-first, good balance)
102
+ src: (url) => buildCloudinaryUrl(url, {
103
+ width: 1000,
104
+ quality: "auto:good",
105
+ crop: "limit",
106
+ }),
107
+ // Full srcset for responsive images
108
+ srcset: (url) => buildCloudinarySrcSet(url, [600, 1000, 1400, 1800, 2400], // Wide range for all devices + retina
109
+ "auto:good"),
110
+ // Hero/featured images (highest quality)
111
+ hero: (url) => buildCloudinaryUrl(url, {
112
+ width: 2400,
113
+ quality: "auto:best",
114
+ crop: "limit",
115
+ }),
116
+ // Thumbnail (for grid previews in admin)
117
+ thumb: (url) => buildCloudinaryUrl(url, {
118
+ width: 400,
119
+ height: 300,
120
+ quality: "auto:eco",
121
+ crop: "fill",
122
+ gravity: "auto",
123
+ }),
124
+ },
125
+ // Avatar/profile images
126
+ avatar: (url) => buildCloudinaryUrl(url, {
127
+ width: 200,
128
+ height: 200,
129
+ quality: "auto:good",
130
+ crop: "fill",
131
+ gravity: "face",
132
+ }),
133
+ };
134
+ /**
135
+ * Helper to check if a URL is from Cloudinary
136
+ */
137
+ export function isCloudinaryUrl(url) {
138
+ return url.includes("res.cloudinary.com") || url.includes("cloudinary.com");
139
+ }
140
+ /**
141
+ * Extract public ID from Cloudinary URL
142
+ */
143
+ export function getCloudinaryPublicId(url) {
144
+ if (!isCloudinaryUrl(url))
145
+ return null;
146
+ const uploadIndex = url.indexOf("/upload/");
147
+ if (uploadIndex === -1)
148
+ return null;
149
+ const pathAfterUpload = url.substring(uploadIndex + 8);
150
+ // Remove file extension
151
+ return pathAfterUpload.replace(/\.[^.]+$/, "");
152
+ }
@@ -48,7 +48,7 @@ export interface CachedModule {
48
48
  instanceKey: string;
49
49
  label: string;
50
50
  slug: string;
51
- dataType: 'collection' | 'singleton' | 'submissions';
51
+ dataType: string;
52
52
  category: string;
53
53
  description?: string;
54
54
  icon?: string;
@@ -0,0 +1,175 @@
1
+ ---
2
+ /**
3
+ * AyezeeForm Component
4
+ *
5
+ * A flexible form component for AyeZee CMS that handles form submission,
6
+ * Turnstile integration, and validation. Fully customizable via slots and props.
7
+ *
8
+ * @example
9
+ * ```astro
10
+ * <AyezeeForm label="Contact Form" formClass="my-form" submitButtonClass="btn-primary">
11
+ * <input type="text" name="name" required />
12
+ * <input type="email" name="email" required />
13
+ * <textarea name="message" required></textarea>
14
+ * </AyezeeForm>
15
+ * ```
16
+ */
17
+
18
+ import { getFormConfig } from '../cms-helper.js';
19
+
20
+ export interface Props {
21
+ /** Module label, slug, or instance key to identify the form */
22
+ label?: string;
23
+ /** Instance key (alternative to label) */
24
+ instanceKey?: string;
25
+ /** Text for the submit button */
26
+ submitButtonText?: string;
27
+ /** CSS classes for the submit button */
28
+ submitButtonClass?: string;
29
+ /** CSS classes for the form element */
30
+ formClass?: string;
31
+ /** Success message to display */
32
+ successMessage?: string;
33
+ /** Error message to display */
34
+ errorMessage?: string;
35
+ /** CSS classes for the message container */
36
+ messageClass?: string;
37
+ /** CSS classes for the Turnstile container */
38
+ turnstileClass?: string;
39
+ /** Whether to show the default submit button (set to false if using custom button in slot) */
40
+ showSubmitButton?: boolean;
41
+ /** Whether to show the default message div (set to false if handling messages externally) */
42
+ showMessageDiv?: boolean;
43
+ /** Additional data attributes to add to the form */
44
+ dataAttributes?: Record<string, string>;
45
+ }
46
+
47
+ const {
48
+ label,
49
+ instanceKey,
50
+ submitButtonText = 'Submit',
51
+ submitButtonClass = '',
52
+ formClass = '',
53
+ successMessage = 'Form submitted successfully!',
54
+ errorMessage = 'Failed to submit form. Please try again.',
55
+ messageClass = '',
56
+ turnstileClass = '',
57
+ showSubmitButton = true,
58
+ showMessageDiv = true,
59
+ dataAttributes = {},
60
+ } = Astro.props;
61
+
62
+ // Get form configuration (includes Turnstile settings)
63
+ const moduleIdentifier = instanceKey || label;
64
+ if (!moduleIdentifier) {
65
+ throw new Error('AyezeeForm requires either "label" or "instanceKey" prop');
66
+ }
67
+
68
+ const formConfig = getFormConfig(moduleIdentifier);
69
+
70
+ if (!formConfig.module) {
71
+ throw new Error(`Form module "${moduleIdentifier}" not found`);
72
+ }
73
+
74
+ // Generate unique form ID
75
+ const formId = `ayezee-form-${formConfig.module.instanceKey}`;
76
+
77
+ // Pass config to client
78
+ const clientConfig = {
79
+ formId,
80
+ apiUrl: formConfig.apiUrl,
81
+ apiKey: formConfig.apiKey,
82
+ turnstile: formConfig.turnstile,
83
+ successMessage,
84
+ errorMessage,
85
+ };
86
+
87
+ // Build data attributes
88
+ const dataAttrs = {
89
+ 'data-ayezee-form': '',
90
+ ...dataAttributes,
91
+ };
92
+ ---
93
+
94
+ <form id={formId} class={formClass} {...dataAttrs}>
95
+ <!-- Honeypot field (hidden from users, catches bots) -->
96
+ <input
97
+ type="text"
98
+ name="website"
99
+ class="absolute -left-[9999px] opacity-0 pointer-events-none"
100
+ tabindex="-1"
101
+ autocomplete="off"
102
+ aria-hidden="true"
103
+ />
104
+
105
+ <!-- Form fields from slot -->
106
+ <slot />
107
+
108
+ <!-- Turnstile container (only rendered if enabled) -->
109
+ {
110
+ formConfig.turnstile.enabled && (
111
+ <div id={`${formId}-turnstile`} class={turnstileClass || 'turnstile-container my-4'} />
112
+ )
113
+ }
114
+
115
+ <!-- Message container -->
116
+ {
117
+ showMessageDiv && (
118
+ <div
119
+ id={`${formId}-message`}
120
+ class={messageClass || 'hidden mt-2 p-3 border rounded text-sm'}
121
+ role="alert"
122
+ aria-live="polite"
123
+ />
124
+ )
125
+ }
126
+
127
+ <!-- Submit button -->
128
+ {
129
+ showSubmitButton && (
130
+ <button
131
+ type="submit"
132
+ id={`${formId}-submit`}
133
+ class={
134
+ submitButtonClass ||
135
+ 'bg-blue-600 text-white px-6 py-3 rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed'
136
+ }
137
+ >
138
+ <span class="button-text">{submitButtonText}</span>
139
+ <span class="button-loading hidden">Submitting...</span>
140
+ </button>
141
+ )
142
+ }
143
+
144
+ <!-- Slot for custom submit button or additional form elements -->
145
+ <slot name="after-form" />
146
+ </form>
147
+
148
+ <!-- Load Turnstile script if enabled -->
149
+ {
150
+ formConfig.turnstile.enabled && (
151
+ <script is:inline src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer />
152
+ )
153
+ }
154
+
155
+ <!-- Pass config to client-side script -->
156
+ <script is:inline define:vars={{ clientConfig }}>
157
+ window[`FORM_CONFIG_${clientConfig.formId}`] = clientConfig;
158
+ </script>
159
+
160
+ <!-- Form handler script -->
161
+ <script>
162
+ import { AyezeeFormHandler, initAyezeeForms } from '../form-handler.js';
163
+
164
+ // Initialize all forms on the page
165
+ function initForms() {
166
+ initAyezeeForms();
167
+ }
168
+
169
+ // Init when DOM is ready
170
+ if (document.readyState === 'loading') {
171
+ document.addEventListener('DOMContentLoaded', initForms);
172
+ } else {
173
+ initForms();
174
+ }
175
+ </script>
@@ -0,0 +1,7 @@
1
+ /**
2
+ * AyeZee CMS Components
3
+ *
4
+ * Export Astro components for use in projects
5
+ */
6
+ export { AyezeeFormHandler, initAyezeeForms, submitToAyezeeCms } from '../form-handler.js';
7
+ export type { FormHandlerConfig, FormSubmissionResult } from '../form-handler.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * AyeZee CMS Components
3
+ *
4
+ * Export Astro components for use in projects
5
+ */
6
+ // Note: Astro components are exported individually, not through this index
7
+ // Import them directly like: import AyezeeForm from 'ayezee-astro-cms/components/AyezeeForm.astro'
8
+ export { AyezeeFormHandler, initAyezeeForms, submitToAyezeeCms } from '../form-handler.js';
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Form Handler Utilities for AyeZee CMS Forms
3
+ *
4
+ * Provides core form submission logic, Turnstile integration, and error handling
5
+ * that can be used in custom form implementations or with the default AyezeeForm component
6
+ */
7
+ export interface FormHandlerConfig {
8
+ formId: string;
9
+ apiUrl: string;
10
+ apiKey: string;
11
+ turnstile: {
12
+ enabled: boolean;
13
+ siteKey: string | null;
14
+ };
15
+ successMessage?: string;
16
+ errorMessage?: string;
17
+ onSuccess?: (data: any) => void;
18
+ onError?: (error: string) => void;
19
+ onSubmitStart?: () => void;
20
+ onSubmitEnd?: () => void;
21
+ }
22
+ export interface FormSubmissionResult {
23
+ success: boolean;
24
+ message: string;
25
+ data?: any;
26
+ errors?: string[];
27
+ }
28
+ /**
29
+ * Core form handler class that manages form submission, validation, and Turnstile integration
30
+ */
31
+ export declare class AyezeeFormHandler {
32
+ private form;
33
+ private submitButton;
34
+ private messageDiv;
35
+ private turnstileContainer;
36
+ private turnstileWidgetId;
37
+ private config;
38
+ constructor(config: FormHandlerConfig);
39
+ private init;
40
+ private initTurnstile;
41
+ private handleSubmit;
42
+ /**
43
+ * Show a message in the message div
44
+ */
45
+ showMessage(message: string, isSuccess: boolean): void;
46
+ /**
47
+ * Hide the message div
48
+ */
49
+ hideMessage(): void;
50
+ /**
51
+ * Set the loading state of the form
52
+ */
53
+ setLoading(isLoading: boolean): void;
54
+ /**
55
+ * Manually submit the form (useful for custom implementations)
56
+ */
57
+ submit(customData?: Record<string, any>): Promise<FormSubmissionResult>;
58
+ /**
59
+ * Reset the form
60
+ */
61
+ reset(): void;
62
+ /**
63
+ * Get form data as an object
64
+ */
65
+ getFormData(): Record<string, any>;
66
+ }
67
+ /**
68
+ * Initialize all AyeZee forms on the page
69
+ * This automatically finds and initializes all forms with [data-ayezee-form] attribute
70
+ */
71
+ export declare function initAyezeeForms(): void;
72
+ /**
73
+ * Standalone function to submit form data to AyeZee CMS API
74
+ * Useful for custom form implementations that don't use the default form handler
75
+ */
76
+ export declare function submitToAyezeeCms(data: Record<string, any>, config: {
77
+ apiUrl: string;
78
+ apiKey: string;
79
+ turnstileToken?: string;
80
+ }): Promise<FormSubmissionResult>;
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Form Handler Utilities for AyeZee CMS Forms
3
+ *
4
+ * Provides core form submission logic, Turnstile integration, and error handling
5
+ * that can be used in custom form implementations or with the default AyezeeForm component
6
+ */
7
+ /**
8
+ * Core form handler class that manages form submission, validation, and Turnstile integration
9
+ */
10
+ export class AyezeeFormHandler {
11
+ form;
12
+ submitButton;
13
+ messageDiv;
14
+ turnstileContainer;
15
+ turnstileWidgetId = null;
16
+ config;
17
+ constructor(config) {
18
+ this.config = config;
19
+ this.form = document.getElementById(config.formId);
20
+ this.submitButton = document.getElementById(`${config.formId}-submit`);
21
+ this.messageDiv = document.getElementById(`${config.formId}-message`);
22
+ this.turnstileContainer = document.getElementById(`${config.formId}-turnstile`);
23
+ if (!this.form) {
24
+ console.error('Form not found:', config.formId);
25
+ return;
26
+ }
27
+ this.init();
28
+ }
29
+ init() {
30
+ // Initialize Turnstile if enabled
31
+ if (this.config.turnstile.enabled && this.turnstileContainer) {
32
+ this.initTurnstile();
33
+ }
34
+ // Attach submit handler
35
+ this.form.addEventListener('submit', (e) => this.handleSubmit(e));
36
+ }
37
+ initTurnstile() {
38
+ if (!window.turnstile) {
39
+ console.warn('Turnstile script not loaded yet, waiting...');
40
+ setTimeout(() => this.initTurnstile(), 100);
41
+ return;
42
+ }
43
+ this.turnstileWidgetId = window.turnstile.render(this.turnstileContainer, {
44
+ sitekey: this.config.turnstile.siteKey,
45
+ theme: 'light',
46
+ size: 'normal',
47
+ });
48
+ }
49
+ async handleSubmit(e) {
50
+ e.preventDefault();
51
+ this.hideMessage();
52
+ this.setLoading(true);
53
+ this.config.onSubmitStart?.();
54
+ try {
55
+ const formData = new FormData(this.form);
56
+ const data = {};
57
+ formData.forEach((value, key) => {
58
+ data[key] = value;
59
+ });
60
+ // Honeypot check
61
+ if (data.website) {
62
+ await new Promise((resolve) => setTimeout(resolve, 1000));
63
+ this.showMessage(this.config.successMessage || 'Form submitted successfully!', true);
64
+ this.form.reset();
65
+ this.config.onSuccess?.(data);
66
+ return;
67
+ }
68
+ // Remove honeypot field before sending to API
69
+ delete data.website;
70
+ // Get Turnstile token if enabled
71
+ if (this.config.turnstile.enabled) {
72
+ const token = window.turnstile?.getResponse(this.turnstileWidgetId);
73
+ if (!token) {
74
+ this.showMessage('Please complete the security challenge.', false);
75
+ this.config.onError?.('Turnstile validation failed');
76
+ return;
77
+ }
78
+ data['cf-turnstile-response'] = token;
79
+ }
80
+ // Submit to API
81
+ console.log('Submitting to:', this.config.apiUrl);
82
+ console.log('Data being sent:', data);
83
+ const response = await fetch(this.config.apiUrl, {
84
+ method: 'POST',
85
+ headers: {
86
+ 'Content-Type': 'application/json',
87
+ Authorization: `Bearer ${this.config.apiKey}`,
88
+ },
89
+ body: JSON.stringify(data),
90
+ });
91
+ const result = await response.json();
92
+ console.log('API Response:', result);
93
+ if (response.ok && result.success) {
94
+ this.showMessage(this.config.successMessage || 'Form submitted successfully!', true);
95
+ this.form.reset();
96
+ // Reset Turnstile
97
+ if (this.turnstileWidgetId !== null) {
98
+ window.turnstile?.reset(this.turnstileWidgetId);
99
+ }
100
+ this.config.onSuccess?.(result);
101
+ }
102
+ else {
103
+ console.error('Submission failed:', result);
104
+ // Parse validation errors
105
+ let errorMessage = this.config.errorMessage || 'Failed to submit form. Please try again.';
106
+ if (result.details) {
107
+ try {
108
+ const details = typeof result.details === 'string' ? JSON.parse(result.details) : result.details;
109
+ if (details.validationErrors && Array.isArray(details.validationErrors)) {
110
+ errorMessage = details.validationErrors.join('. ');
111
+ }
112
+ }
113
+ catch (e) {
114
+ console.error('Error parsing validation details:', e);
115
+ }
116
+ }
117
+ if (!errorMessage || errorMessage === this.config.errorMessage) {
118
+ errorMessage = result.error || result.message || this.config.errorMessage || 'An error occurred';
119
+ }
120
+ this.showMessage(errorMessage, false);
121
+ this.config.onError?.(errorMessage);
122
+ }
123
+ }
124
+ catch (error) {
125
+ console.error('Form submission error:', error);
126
+ const errorMsg = 'Network error. Please try again.';
127
+ this.showMessage(errorMsg, false);
128
+ this.config.onError?.(errorMsg);
129
+ }
130
+ finally {
131
+ this.setLoading(false);
132
+ this.config.onSubmitEnd?.();
133
+ }
134
+ }
135
+ /**
136
+ * Show a message in the message div
137
+ */
138
+ showMessage(message, isSuccess) {
139
+ if (!this.messageDiv)
140
+ return;
141
+ this.messageDiv.textContent = message;
142
+ this.messageDiv.classList.remove('hidden');
143
+ if (isSuccess) {
144
+ this.messageDiv.classList.remove('border-red-300', 'bg-red-50', 'text-red-700');
145
+ this.messageDiv.classList.add('border-green-300', 'bg-green-50', 'text-green-700');
146
+ }
147
+ else {
148
+ this.messageDiv.classList.remove('border-green-300', 'bg-green-50', 'text-green-700');
149
+ this.messageDiv.classList.add('border-red-300', 'bg-red-50', 'text-red-700');
150
+ }
151
+ }
152
+ /**
153
+ * Hide the message div
154
+ */
155
+ hideMessage() {
156
+ if (!this.messageDiv)
157
+ return;
158
+ this.messageDiv.classList.add('hidden');
159
+ }
160
+ /**
161
+ * Set the loading state of the form
162
+ */
163
+ setLoading(isLoading) {
164
+ if (!this.submitButton)
165
+ return;
166
+ this.submitButton.disabled = isLoading;
167
+ const textSpan = this.submitButton.querySelector('.button-text');
168
+ const loadingSpan = this.submitButton.querySelector('.button-loading');
169
+ if (isLoading) {
170
+ textSpan?.classList.add('hidden');
171
+ loadingSpan?.classList.remove('hidden');
172
+ }
173
+ else {
174
+ textSpan?.classList.remove('hidden');
175
+ loadingSpan?.classList.add('hidden');
176
+ }
177
+ }
178
+ /**
179
+ * Manually submit the form (useful for custom implementations)
180
+ */
181
+ async submit(customData) {
182
+ // If custom data provided, temporarily populate form
183
+ if (customData) {
184
+ Object.entries(customData).forEach(([key, value]) => {
185
+ const input = this.form.querySelector(`[name="${key}"]`);
186
+ if (input) {
187
+ input.value = String(value);
188
+ }
189
+ });
190
+ }
191
+ // Trigger form submission
192
+ const event = new Event('submit', { cancelable: true });
193
+ this.form.dispatchEvent(event);
194
+ return {
195
+ success: true,
196
+ message: 'Form submitted',
197
+ };
198
+ }
199
+ /**
200
+ * Reset the form
201
+ */
202
+ reset() {
203
+ this.form.reset();
204
+ this.hideMessage();
205
+ if (this.turnstileWidgetId !== null) {
206
+ window.turnstile?.reset(this.turnstileWidgetId);
207
+ }
208
+ }
209
+ /**
210
+ * Get form data as an object
211
+ */
212
+ getFormData() {
213
+ const formData = new FormData(this.form);
214
+ const data = {};
215
+ formData.forEach((value, key) => {
216
+ data[key] = value;
217
+ });
218
+ return data;
219
+ }
220
+ }
221
+ /**
222
+ * Initialize all AyeZee forms on the page
223
+ * This automatically finds and initializes all forms with [data-ayezee-form] attribute
224
+ */
225
+ export function initAyezeeForms() {
226
+ const forms = document.querySelectorAll('[data-ayezee-form]');
227
+ forms.forEach((form) => {
228
+ const formId = form.id;
229
+ const config = window[`FORM_CONFIG_${formId}`];
230
+ if (config) {
231
+ new AyezeeFormHandler(config);
232
+ }
233
+ });
234
+ }
235
+ /**
236
+ * Standalone function to submit form data to AyeZee CMS API
237
+ * Useful for custom form implementations that don't use the default form handler
238
+ */
239
+ export async function submitToAyezeeCms(data, config) {
240
+ try {
241
+ const payload = { ...data };
242
+ if (config.turnstileToken) {
243
+ payload['cf-turnstile-response'] = config.turnstileToken;
244
+ }
245
+ const response = await fetch(config.apiUrl, {
246
+ method: 'POST',
247
+ headers: {
248
+ 'Content-Type': 'application/json',
249
+ Authorization: `Bearer ${config.apiKey}`,
250
+ },
251
+ body: JSON.stringify(payload),
252
+ });
253
+ const result = await response.json();
254
+ if (response.ok && result.success) {
255
+ return {
256
+ success: true,
257
+ message: result.message || 'Form submitted successfully!',
258
+ data: result.data,
259
+ };
260
+ }
261
+ else {
262
+ // Parse validation errors
263
+ let errors = [];
264
+ if (result.details) {
265
+ try {
266
+ const details = typeof result.details === 'string' ? JSON.parse(result.details) : result.details;
267
+ if (details.validationErrors && Array.isArray(details.validationErrors)) {
268
+ errors = details.validationErrors;
269
+ }
270
+ }
271
+ catch (e) {
272
+ console.error('Error parsing validation details:', e);
273
+ }
274
+ }
275
+ return {
276
+ success: false,
277
+ message: result.error || result.message || 'Failed to submit form',
278
+ errors: errors.length > 0 ? errors : undefined,
279
+ };
280
+ }
281
+ }
282
+ catch (error) {
283
+ console.error('Form submission error:', error);
284
+ return {
285
+ success: false,
286
+ message: 'Network error. Please try again.',
287
+ };
288
+ }
289
+ }
package/dist/index.d.ts CHANGED
@@ -6,5 +6,7 @@
6
6
  * @packageDocumentation
7
7
  */
8
8
  export * from './cms-helper.js';
9
+ export * from './cloudinary-utils.js';
10
+ export * from './form-handler.js';
9
11
  export { ayezeeCms, type AyezeeCmsOptions } from './integration.js';
10
12
  export { ayezeeCms as default } from './integration.js';
package/dist/index.js CHANGED
@@ -7,6 +7,10 @@
7
7
  */
8
8
  // Export CMS helper functions and types
9
9
  export * from './cms-helper.js';
10
+ // Export Cloudinary utilities
11
+ export * from './cloudinary-utils.js';
12
+ // Export form handler utilities
13
+ export * from './form-handler.js';
10
14
  // Export integration
11
15
  export { ayezeeCms } from './integration.js';
12
16
  // Default export
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ayezee-astro-cms",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "AyeZee CMS integration for Astro with automatic data fetching, form handling, and validation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,6 +17,11 @@
17
17
  "./components": {
18
18
  "types": "./dist/components/index.d.ts",
19
19
  "import": "./dist/components/index.js"
20
+ },
21
+ "./components/AyezeeForm.astro": "./dist/components/AyezeeForm.astro",
22
+ "./form-handler": {
23
+ "types": "./dist/form-handler.d.ts",
24
+ "import": "./dist/form-handler.js"
20
25
  }
21
26
  },
22
27
  "files": [
@@ -24,7 +29,8 @@
24
29
  "README.md"
25
30
  ],
26
31
  "scripts": {
27
- "build": "tsc",
32
+ "build": "tsc && npm run copy-astro",
33
+ "copy-astro": "cp src/components/*.astro dist/components/ 2>/dev/null || true",
28
34
  "dev": "tsc --watch",
29
35
  "prepublishOnly": "npm run build"
30
36
  },