perspectapi-ts-sdk 6.5.7 → 7.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.
Files changed (58) hide show
  1. package/README.md +46 -1011
  2. package/dist/chunk-K3T2AFYA.mjs +1393 -0
  3. package/dist/index-CWvUyMt3.d.mts +2224 -0
  4. package/dist/index-CWvUyMt3.d.ts +2224 -0
  5. package/dist/index.d.mts +130 -2201
  6. package/dist/index.d.ts +130 -2201
  7. package/dist/index.js +15 -2
  8. package/dist/index.mjs +13 -1357
  9. package/dist/v2/index.d.mts +1 -0
  10. package/dist/v2/index.d.ts +1 -0
  11. package/dist/v2/index.js +1419 -0
  12. package/dist/v2/index.mjs +40 -0
  13. package/docs/README.md +15 -0
  14. package/docs/v1-deprecated/README.md +9 -0
  15. package/docs/v1-deprecated/examples/README.md +324 -0
  16. package/docs/v1-deprecated/examples/basic-usage.ts +258 -0
  17. package/docs/v1-deprecated/examples/cloudflare-worker.ts +274 -0
  18. package/docs/v1-deprecated/examples/content-query-with-slug-prefix.ts +237 -0
  19. package/docs/v1-deprecated/examples/image-transforms.ts +200 -0
  20. package/docs/v1-deprecated/examples/site-user-checkout.ts +186 -0
  21. package/docs/v1-deprecated/examples/slug-prefix-examples.ts +491 -0
  22. package/docs/v1-deprecated/legacy-docs/caching.md +667 -0
  23. package/docs/v1-deprecated/legacy-docs/contact.md +1396 -0
  24. package/docs/v1-deprecated/legacy-docs/csrf-protection.md +664 -0
  25. package/docs/v1-deprecated/legacy-docs/image-transforms.md +523 -0
  26. package/docs/v1-deprecated/legacy-docs/loaders.md +304 -0
  27. package/docs/v1-deprecated/legacy-docs/newsletter.md +811 -0
  28. package/docs/v1-deprecated/legacy-docs/site-users.md +817 -0
  29. package/docs/v1-deprecated/legacy-notes/CHANGELOG-CHECKOUT.md +143 -0
  30. package/docs/v1-deprecated/legacy-notes/CSRF-CHECKOUT.md +271 -0
  31. package/docs/v1-deprecated/legacy-notes/IMAGE_TRANSFORMS_PORT.md +298 -0
  32. package/docs/v1-deprecated/sdk-readme.md +1076 -0
  33. package/examples/README.md +19 -0
  34. package/examples/basic-v2.ts +37 -0
  35. package/llms.txt +25 -0
  36. package/package.json +18 -7
  37. package/src/client/api-keys-client.ts +4 -0
  38. package/src/client/auth-client.ts +4 -0
  39. package/src/client/base-client.ts +7 -0
  40. package/src/client/bundles-client.ts +4 -0
  41. package/src/client/categories-client.ts +4 -0
  42. package/src/client/checkout-client.ts +4 -0
  43. package/src/client/contact-client.ts +4 -0
  44. package/src/client/content-client.ts +4 -0
  45. package/src/client/newsletter-client.ts +4 -0
  46. package/src/client/newsletter-management-client.ts +4 -0
  47. package/src/client/organizations-client.ts +4 -0
  48. package/src/client/products-client.ts +4 -0
  49. package/src/client/site-users-client.ts +10 -1
  50. package/src/client/sites-client.ts +4 -0
  51. package/src/client/webhooks-client.ts +4 -0
  52. package/src/deprecation.ts +2 -1
  53. package/src/index.ts +2 -1
  54. package/src/loaders.ts +59 -0
  55. package/src/perspect-api-client.ts +2 -2
  56. package/src/v2/client/orders-client.ts +6 -1
  57. package/src/v2/client/subscriptions-client.ts +15 -0
  58. package/src/v2/types.ts +23 -0
@@ -0,0 +1,1396 @@
1
+ # Contact Form API Documentation
2
+
3
+ > Deprecated v1 material. Do not copy these examples into new code. v1 sunsets
4
+ > on 2026-06-01; use `createPerspectApiV2Client` from
5
+ > `perspectapi-ts-sdk/v2` and `/api/v2`.
6
+
7
+ The PerspectAPI SDK provides a robust contact form system with spam prevention, rate limiting, CAPTCHA support, and comprehensive admin management capabilities.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Quick Start](#quick-start)
12
+ - [Core Features](#core-features)
13
+ - [API Reference](#api-reference)
14
+ - [Public Methods](#public-methods)
15
+ - [Admin Methods](#admin-methods)
16
+ - [Security Features](#security-features)
17
+ - [Best Practices](#best-practices)
18
+ - [Examples](#examples)
19
+ - [Error Handling](#error-handling)
20
+
21
+ ## Quick Start
22
+
23
+ ```typescript
24
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
25
+
26
+ const client = createPerspectApiClient({
27
+ baseUrl: 'https://api.example.com',
28
+ apiKey: 'your-api-key'
29
+ });
30
+
31
+ // Browser-based submission with CSRF protection
32
+ const csrfResponse = await client.getCsrfToken('my-site');
33
+ const csrfToken = csrfResponse.data.token;
34
+
35
+ // Submit a contact form with CSRF token
36
+ const submission = await client.contact.submitContact('my-site', {
37
+ name: 'John Doe',
38
+ email: 'john@example.com',
39
+ subject: 'Product Inquiry',
40
+ message: 'I would like to know more about your products.'
41
+ }, csrfToken); // Pass CSRF token for security
42
+ ```
43
+
44
+ ## Core Features
45
+
46
+ - **Spam Prevention**: Multiple layers including honeypot fields, rate limiting, and CAPTCHA
47
+ - **Cloudflare Turnstile**: Built-in support for privacy-focused CAPTCHA
48
+ - **Rate Limiting**: Automatic prevention of abuse (5 submissions per 15 minutes per IP/email)
49
+ - **Email Notifications**: Automatic notifications to site owners
50
+ - **Status Tracking**: Track submission status (pending, read, archived)
51
+ - **Admin Management**: Complete admin interface for managing submissions
52
+ - **Bulk Operations**: Process multiple submissions at once
53
+ - **Export Capability**: Export submissions in various formats
54
+ - **Custom Fields**: Support for additional metadata
55
+
56
+ ## API Reference
57
+
58
+ ### Public Methods
59
+
60
+ #### `submitContact(siteName, data)`
61
+
62
+ Submit a contact form to a specific site.
63
+
64
+ ```typescript
65
+ interface CreateContactRequest {
66
+ name: string; // Required: Full name
67
+ email: string; // Required: Valid email address
68
+ subject?: string; // Optional: Subject line
69
+ message: string; // Required: Message content
70
+ phone?: string; // Optional: Phone number
71
+ company?: string; // Optional: Company name
72
+ turnstileToken?: string; // Optional: Cloudflare Turnstile response
73
+ metadata?: Record<string, any>; // Optional: Additional custom data
74
+ }
75
+
76
+ // Example with CSRF token (for browser-based forms)
77
+ const csrfToken = (await client.getCsrfToken('my-site')).data.token;
78
+
79
+ const submission = await client.contact.submitContact('my-site', {
80
+ name: 'Jane Smith',
81
+ email: 'jane@company.com',
82
+ subject: 'Partnership Opportunity',
83
+ message: 'We are interested in partnering with your company...',
84
+ phone: '+1-555-0123',
85
+ company: 'Tech Corp',
86
+ turnstileToken: 'turnstile-response-token',
87
+ metadata: {
88
+ source: 'contact-page',
89
+ campaign: 'q4-2024'
90
+ }
91
+ }, csrfToken); // Pass CSRF token
92
+
93
+ // Response
94
+ {
95
+ success: true,
96
+ code: "CONTACT_SUBMITTED",
97
+ contact_id: "contact_abc123",
98
+ message: "Contact form submitted successfully",
99
+ status: "submitted"
100
+ }
101
+ ```
102
+
103
+ #### `getContactStatus(siteName, id)`
104
+
105
+ Check the status of a contact submission.
106
+
107
+ ```typescript
108
+ const status = await client.contact.getContactStatus('my-site', 'contact_abc123');
109
+
110
+ // Response
111
+ {
112
+ success: true,
113
+ code: "CONTACT_FOUND",
114
+ contact_id: "contact_abc123",
115
+ status: "pending", // or "read", "replied", "archived"
116
+ submitted_at: "2024-01-15T10:30:00Z",
117
+ processed_at: null,
118
+ metadata: {}
119
+ }
120
+ ```
121
+
122
+ ### Admin Methods
123
+
124
+ #### `getContactSubmissions(siteName, params)`
125
+
126
+ Get all contact submissions with filtering and pagination.
127
+
128
+ ```typescript
129
+ const submissions = await client.contact.getContactSubmissions('my-site', {
130
+ page: 1,
131
+ limit: 50,
132
+ status: 'unread', // Filter by status
133
+ startDate: '2024-01-01', // Date range start
134
+ endDate: '2024-01-31', // Date range end
135
+ search: 'product' // Search in name, email, subject, message
136
+ });
137
+
138
+ // Response includes paginated list of submissions
139
+ {
140
+ data: [
141
+ {
142
+ id: "contact_123",
143
+ name: "John Doe",
144
+ email: "john@example.com",
145
+ subject: "Product Question",
146
+ message: "...",
147
+ status: "unread",
148
+ createdAt: "2024-01-15T10:30:00Z"
149
+ }
150
+ // ...
151
+ ],
152
+ pagination: {
153
+ page: 1,
154
+ limit: 50,
155
+ total: 234,
156
+ totalPages: 5
157
+ }
158
+ }
159
+ ```
160
+
161
+ #### `getContactSubmissionById(siteName, id)`
162
+
163
+ Get detailed information about a specific submission.
164
+
165
+ ```typescript
166
+ const submission = await client.contact.getContactSubmissionById(
167
+ 'my-site',
168
+ 'contact_abc123'
169
+ );
170
+
171
+ // Response includes full details
172
+ {
173
+ id: "contact_abc123",
174
+ name: "Jane Smith",
175
+ email: "jane@example.com",
176
+ subject: "Partnership Inquiry",
177
+ message: "Full message content...",
178
+ phone: "+1-555-0123",
179
+ company: "Tech Corp",
180
+ status: "pending",
181
+ ipAddress: "203.0.113.0",
182
+ userAgent: "Mozilla/5.0...",
183
+ metadata: {
184
+ source: "contact-page",
185
+ campaign: "q4-2024"
186
+ },
187
+ createdAt: "2024-01-15T10:30:00Z"
188
+ }
189
+ ```
190
+
191
+ #### `updateContactStatus(siteName, id, status, notes?)`
192
+
193
+ Update the status of a contact submission.
194
+
195
+ ```typescript
196
+ await client.contact.updateContactStatus(
197
+ 'my-site',
198
+ 'contact_abc123',
199
+ 'replied',
200
+ 'Responded via email on Jan 16'
201
+ );
202
+ ```
203
+
204
+ #### `markContactAsRead(siteName, id)`
205
+
206
+ Mark a submission as read.
207
+
208
+ ```typescript
209
+ await client.contact.markContactAsRead('my-site', 'contact_abc123');
210
+ ```
211
+
212
+ #### `markContactAsUnread(siteName, id)`
213
+
214
+ Mark a submission as unread.
215
+
216
+ ```typescript
217
+ await client.contact.markContactAsUnread('my-site', 'contact_abc123');
218
+ ```
219
+
220
+ #### `archiveContact(siteName, id)`
221
+
222
+ Archive a contact submission.
223
+
224
+ ```typescript
225
+ await client.contact.archiveContact('my-site', 'contact_abc123');
226
+ ```
227
+
228
+ #### `deleteContact(siteName, id)`
229
+
230
+ Permanently delete a contact submission.
231
+
232
+ ```typescript
233
+ await client.contact.deleteContact('my-site', 'contact_abc123');
234
+ ```
235
+
236
+ #### `bulkUpdateContacts(siteName, data)`
237
+
238
+ Perform bulk operations on multiple submissions.
239
+
240
+ ```typescript
241
+ const result = await client.contact.bulkUpdateContacts('my-site', {
242
+ ids: ['contact_1', 'contact_2', 'contact_3'],
243
+ action: 'mark_read' // or 'mark_unread', 'archive', 'delete'
244
+ });
245
+
246
+ // Response
247
+ {
248
+ success: true,
249
+ updatedCount: 3,
250
+ failedCount: 0
251
+ }
252
+ ```
253
+
254
+ #### `getContactStats(siteName, params)`
255
+
256
+ Get contact form statistics.
257
+
258
+ ```typescript
259
+ const stats = await client.contact.getContactStats('my-site', {
260
+ startDate: '2024-01-01',
261
+ endDate: '2024-01-31'
262
+ });
263
+
264
+ // Response
265
+ {
266
+ totalSubmissions: 234,
267
+ unreadSubmissions: 12,
268
+ readSubmissions: 200,
269
+ archivedSubmissions: 22,
270
+ submissionsByDay: [
271
+ { date: "2024-01-01", count: 8 },
272
+ { date: "2024-01-02", count: 12 },
273
+ // ...
274
+ ],
275
+ submissionsByStatus: {
276
+ pending: 12,
277
+ read: 200,
278
+ archived: 22
279
+ },
280
+ averageResponseTime: 3600000 // milliseconds
281
+ }
282
+ ```
283
+
284
+ #### `exportContacts(siteName, params)`
285
+
286
+ Export contact submissions to various formats.
287
+
288
+ ```typescript
289
+ const exportData = await client.contact.exportContacts('my-site', {
290
+ format: 'csv', // or 'json', 'xlsx'
291
+ startDate: '2024-01-01',
292
+ endDate: '2024-01-31',
293
+ status: 'all'
294
+ });
295
+
296
+ // Response
297
+ {
298
+ downloadUrl: "https://api.example.com/exports/contacts_export_123.csv",
299
+ expiresAt: "2024-01-16T10:30:00Z"
300
+ }
301
+ ```
302
+
303
+ #### `getContactConfig(siteName)`
304
+
305
+ Get contact form configuration.
306
+
307
+ ```typescript
308
+ const config = await client.contact.getContactConfig('my-site');
309
+
310
+ // Response
311
+ {
312
+ turnstileEnabled: true,
313
+ rateLimitEnabled: true,
314
+ rateLimitWindow: 900, // seconds (15 minutes)
315
+ rateLimitMax: 5,
316
+ requiredFields: ['name', 'email', 'message'],
317
+ customFields: [
318
+ {
319
+ name: 'department',
320
+ type: 'select',
321
+ required: false,
322
+ label: 'Department'
323
+ }
324
+ ]
325
+ }
326
+ ```
327
+
328
+ #### `updateContactConfig(siteName, config)`
329
+
330
+ Update contact form configuration (admin only).
331
+
332
+ ```typescript
333
+ await client.contact.updateContactConfig('my-site', {
334
+ turnstileEnabled: true,
335
+ rateLimitEnabled: true,
336
+ rateLimitWindow: 600, // 10 minutes
337
+ rateLimitMax: 3,
338
+ requiredFields: ['name', 'email', 'message', 'phone'],
339
+ customFields: [
340
+ {
341
+ name: 'urgency',
342
+ type: 'select',
343
+ required: true,
344
+ label: 'How urgent is your request?'
345
+ }
346
+ ]
347
+ });
348
+ ```
349
+
350
+ ## Response Format
351
+
352
+ All contact API methods return responses in a standardized format with consistent success/error handling:
353
+
354
+ ### Success Responses
355
+
356
+ All successful responses include:
357
+ - `success: true` - Indicates the operation completed successfully
358
+ - `code: string` - Machine-readable status code for programmatic handling
359
+ - `message?: string` - Human-readable message for display to users (optional)
360
+ - Additional response-specific data
361
+
362
+ ```typescript
363
+ // Successful contact submission
364
+ {
365
+ success: true,
366
+ code: "CONTACT_SUBMITTED",
367
+ contact_id: "contact_abc123",
368
+ message: "Contact form submitted successfully",
369
+ status: "submitted"
370
+ }
371
+
372
+ // Contact status lookup
373
+ {
374
+ success: true,
375
+ code: "CONTACT_FOUND",
376
+ contact_id: "contact_abc123",
377
+ status: "pending",
378
+ submitted_at: "2024-01-15T10:30:00Z",
379
+ processed_at: null,
380
+ metadata: {}
381
+ }
382
+
383
+ // Bulk operations
384
+ {
385
+ success: true,
386
+ code: "CONTACTS_UPDATED",
387
+ updatedCount: 5,
388
+ failedCount: 0
389
+ }
390
+ ```
391
+
392
+ ### Error Responses
393
+
394
+ Failed operations include:
395
+ - `success: false` - Indicates the operation failed
396
+ - `code: string` - Machine-readable error code
397
+ - `error: string` - Error message describing what went wrong
398
+ - `details?: any` - Additional error details (for validation errors)
399
+
400
+ ```typescript
401
+ // Rate limiting
402
+ {
403
+ success: false,
404
+ code: "RATE_LIMITED",
405
+ error: "Rate limit exceeded. Please try again later.",
406
+ remaining_submissions: 0
407
+ }
408
+
409
+ // Validation error
410
+ {
411
+ success: false,
412
+ code: "VALIDATION_ERROR",
413
+ error: "Validation failed",
414
+ details: {
415
+ fieldErrors: {
416
+ email: ["Invalid email format"],
417
+ message: ["Message is required"]
418
+ }
419
+ }
420
+ }
421
+
422
+ // Contact not found
423
+ {
424
+ success: false,
425
+ code: "CONTACT_NOT_FOUND",
426
+ error: "Contact not found"
427
+ }
428
+ ```
429
+
430
+ ### Response Codes
431
+
432
+ #### Success Codes (`success: true`)
433
+ - `CONTACT_SUBMITTED` - Contact form submitted successfully
434
+ - `CONTACT_FOUND` - Contact record retrieved
435
+ - `CONTACTS_LISTED` - Contact list retrieved
436
+ - `CONTACTS_UPDATED` - Bulk update completed
437
+
438
+ #### Error Codes (`success: false`)
439
+ - `INVALID_BODY` - Malformed request data
440
+ - `VALIDATION_ERROR` - Field validation failed
441
+ - `SITE_NOT_FOUND` - Site not found
442
+ - `RATE_LIMITED` - Too many submissions
443
+ - `CONTACT_NOT_FOUND` - Contact record not found
444
+ - `MISSING_SITE_ID` - Site ID parameter missing
445
+ - `UNAUTHORIZED` - Invalid authentication
446
+ - `DATABASE_ERROR` - Database operation failed
447
+ - `SERVER_ERROR` - Internal server error
448
+
449
+ ### Handling Responses
450
+
451
+ ```typescript
452
+ const result = await client.contact.submitContact('my-site', {
453
+ name: 'John Doe',
454
+ email: 'john@example.com',
455
+ message: 'Hello world'
456
+ }, csrfToken);
457
+
458
+ // Type-safe response handling
459
+ if (result.data?.success) {
460
+ switch (result.data.code) {
461
+ case 'CONTACT_SUBMITTED':
462
+ showSuccess('Your message has been sent successfully!');
463
+ // Redirect to thank you page
464
+ break;
465
+ }
466
+ } else {
467
+ switch (result.data?.code) {
468
+ case 'RATE_LIMITED':
469
+ showError('Please wait before sending another message');
470
+ break;
471
+ case 'VALIDATION_ERROR':
472
+ showValidationErrors(result.data.details);
473
+ break;
474
+ default:
475
+ showError(result.data?.error || 'Something went wrong');
476
+ }
477
+ }
478
+ ```
479
+
480
+ ## Security Features
481
+
482
+ ### 1. Cloudflare Turnstile Integration
483
+
484
+ ```typescript
485
+ // Client-side: Get Turnstile token
486
+ const turnstileToken = await getTurnstileResponse();
487
+
488
+ // Include token in submission
489
+ const submission = await client.contact.submitContact('my-site', {
490
+ name: 'John Doe',
491
+ email: 'john@example.com',
492
+ message: 'Hello',
493
+ turnstileToken // Required when Turnstile is enabled
494
+ });
495
+ ```
496
+
497
+ ### 2. Honeypot Field
498
+
499
+ The API automatically handles honeypot fields to catch bots:
500
+
501
+ ```typescript
502
+ // In your HTML form
503
+ <input type="text" name="hp" style="display: none" />
504
+
505
+ // The SDK handles this automatically
506
+ // If honeypot is filled, submission is silently rejected
507
+ ```
508
+
509
+ ### 3. Rate Limiting
510
+
511
+ Built-in rate limiting prevents abuse:
512
+ - 5 submissions per 15 minutes per IP/email combination
513
+ - Returns 429 status when limit exceeded
514
+ - Automatic cleanup of old rate limit records
515
+
516
+ ### 4. CSRF Protection
517
+
518
+ The API requires CSRF tokens for browser-based submissions:
519
+
520
+ ```typescript
521
+ // Fetch CSRF token with proper credentials
522
+ const csrfResponse = await fetch('https://api.example.com/api/v1/csrf/token/my-site', {
523
+ credentials: 'same-origin', // Include cookies for session
524
+ headers: {
525
+ 'X-API-Key': 'your-api-key'
526
+ }
527
+ });
528
+ const csrfData = await csrfResponse.json();
529
+ const csrfToken = csrfData.token;
530
+
531
+ // Use token in SDK
532
+ const submission = await client.contact.submitContact('my-site', formData, csrfToken);
533
+ ```
534
+
535
+ **Important**: Never auto-fetch CSRF tokens in the SDK itself, as this defeats the security purpose. The client application must fetch and provide the token.
536
+
537
+ ## Best Practices
538
+
539
+ ### 1. Always Use Turnstile for Public Forms
540
+
541
+ ```typescript
542
+ // React component example
543
+ function ContactForm() {
544
+ const [turnstileToken, setTurnstileToken] = useState('');
545
+
546
+ return (
547
+ <form onSubmit={handleSubmit}>
548
+ {/* Form fields */}
549
+
550
+ <Turnstile
551
+ sitekey={process.env.REACT_APP_TURNSTILE_SITE_KEY}
552
+ onVerify={(token) => setTurnstileToken(token)}
553
+ />
554
+
555
+ <button type="submit">Send Message</button>
556
+ </form>
557
+ );
558
+ }
559
+ ```
560
+
561
+ ### 2. Implement Proper Error Handling
562
+
563
+ ```typescript
564
+ try {
565
+ const submission = await client.contact.submitContact('my-site', formData);
566
+ showSuccess('Message sent successfully!');
567
+ } catch (error) {
568
+ if (error.status === 429) {
569
+ showError('Too many submissions. Please wait before trying again.');
570
+ } else if (error.status === 400) {
571
+ showError('Please check your input and try again.');
572
+ } else {
573
+ showError('Something went wrong. Please try again later.');
574
+ }
575
+ }
576
+ ```
577
+
578
+ ### 3. Validate on Both Client and Server
579
+
580
+ ```typescript
581
+ // Client-side validation
582
+ function validateForm(data) {
583
+ const errors = {};
584
+
585
+ if (!data.name || data.name.length < 2) {
586
+ errors.name = 'Name must be at least 2 characters';
587
+ }
588
+
589
+ if (!isValidEmail(data.email)) {
590
+ errors.email = 'Please enter a valid email';
591
+ }
592
+
593
+ if (!data.message || data.message.length < 10) {
594
+ errors.message = 'Message must be at least 10 characters';
595
+ }
596
+
597
+ return errors;
598
+ }
599
+
600
+ // Server validates automatically via the API
601
+ ```
602
+
603
+ ### 4. Implement Admin Notifications
604
+
605
+ ```typescript
606
+ // Set up webhook or polling for new submissions
607
+ async function checkNewSubmissions() {
608
+ const submissions = await client.contact.getContactSubmissions('my-site', {
609
+ status: 'unread',
610
+ limit: 10
611
+ });
612
+
613
+ if (submissions.data.length > 0) {
614
+ // Send notification to admin
615
+ notifyAdmin(`You have ${submissions.data.length} new messages`);
616
+ }
617
+ }
618
+
619
+ // Run every 5 minutes
620
+ setInterval(checkNewSubmissions, 5 * 60 * 1000);
621
+ ```
622
+
623
+ ## Examples
624
+
625
+ ### React Contact Form Component
626
+
627
+ ```tsx
628
+ import React, { useState } from 'react';
629
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
630
+ import { Turnstile } from '@marsidev/react-turnstile';
631
+
632
+ interface FormData {
633
+ name: string;
634
+ email: string;
635
+ subject: string;
636
+ message: string;
637
+ phone?: string;
638
+ company?: string;
639
+ }
640
+
641
+ export function ContactForm() {
642
+ const [formData, setFormData] = useState<FormData>({
643
+ name: '',
644
+ email: '',
645
+ subject: '',
646
+ message: ''
647
+ });
648
+ const [turnstileToken, setTurnstileToken] = useState('');
649
+ const [csrfToken, setCsrfToken] = useState<string | null>(null);
650
+ const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
651
+ const [errorMessage, setErrorMessage] = useState('');
652
+
653
+ const client = createPerspectApiClient({
654
+ baseUrl: process.env.REACT_APP_API_URL!,
655
+ apiKey: process.env.REACT_APP_API_KEY!
656
+ });
657
+
658
+ // Fetch CSRF token on component mount
659
+ useEffect(() => {
660
+ async function fetchCsrfToken() {
661
+ try {
662
+ const response = await client.getCsrfToken('my-site');
663
+ setCsrfToken(response.data.token || '');
664
+ } catch (error) {
665
+ console.error('Failed to fetch CSRF token:', error);
666
+ }
667
+ }
668
+ fetchCsrfToken();
669
+ }, []);
670
+
671
+ const handleInputChange = (
672
+ e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
673
+ ) => {
674
+ const { name, value } = e.target;
675
+ setFormData(prev => ({ ...prev, [name]: value }));
676
+ };
677
+
678
+ const handleSubmit = async (e: React.FormEvent) => {
679
+ e.preventDefault();
680
+
681
+ if (!csrfToken) {
682
+ setErrorMessage('Security token not loaded. Please refresh and try again.');
683
+ return;
684
+ }
685
+
686
+ setStatus('submitting');
687
+ setErrorMessage('');
688
+
689
+ try {
690
+ const result = await client.contact.submitContact('my-site', {
691
+ ...formData,
692
+ turnstileToken,
693
+ metadata: {
694
+ source: 'contact-page',
695
+ timestamp: new Date().toISOString()
696
+ }
697
+ }, csrfToken); // Pass CSRF token
698
+
699
+ setStatus('success');
700
+ setFormData({
701
+ name: '',
702
+ email: '',
703
+ subject: '',
704
+ message: ''
705
+ });
706
+
707
+ // Show success message
708
+ alert('Thank you for your message! We\'ll get back to you soon.');
709
+ } catch (error: any) {
710
+ setStatus('error');
711
+
712
+ if (error.status === 429) {
713
+ setErrorMessage('Too many submissions. Please wait a few minutes.');
714
+ } else if (error.status === 400) {
715
+ setErrorMessage('Please check your input and try again.');
716
+ } else {
717
+ setErrorMessage('Something went wrong. Please try again later.');
718
+ }
719
+ }
720
+ };
721
+
722
+ return (
723
+ <form onSubmit={handleSubmit} className="contact-form">
724
+ <div className="form-group">
725
+ <label htmlFor="name">Name *</label>
726
+ <input
727
+ type="text"
728
+ id="name"
729
+ name="name"
730
+ value={formData.name}
731
+ onChange={handleInputChange}
732
+ required
733
+ disabled={status === 'submitting'}
734
+ />
735
+ </div>
736
+
737
+ <div className="form-group">
738
+ <label htmlFor="email">Email *</label>
739
+ <input
740
+ type="email"
741
+ id="email"
742
+ name="email"
743
+ value={formData.email}
744
+ onChange={handleInputChange}
745
+ required
746
+ disabled={status === 'submitting'}
747
+ />
748
+ </div>
749
+
750
+ <div className="form-group">
751
+ <label htmlFor="phone">Phone</label>
752
+ <input
753
+ type="tel"
754
+ id="phone"
755
+ name="phone"
756
+ value={formData.phone || ''}
757
+ onChange={handleInputChange}
758
+ disabled={status === 'submitting'}
759
+ />
760
+ </div>
761
+
762
+ <div className="form-group">
763
+ <label htmlFor="company">Company</label>
764
+ <input
765
+ type="text"
766
+ id="company"
767
+ name="company"
768
+ value={formData.company || ''}
769
+ onChange={handleInputChange}
770
+ disabled={status === 'submitting'}
771
+ />
772
+ </div>
773
+
774
+ <div className="form-group">
775
+ <label htmlFor="subject">Subject</label>
776
+ <input
777
+ type="text"
778
+ id="subject"
779
+ name="subject"
780
+ value={formData.subject}
781
+ onChange={handleInputChange}
782
+ disabled={status === 'submitting'}
783
+ />
784
+ </div>
785
+
786
+ <div className="form-group">
787
+ <label htmlFor="message">Message *</label>
788
+ <textarea
789
+ id="message"
790
+ name="message"
791
+ value={formData.message}
792
+ onChange={handleInputChange}
793
+ rows={5}
794
+ required
795
+ disabled={status === 'submitting'}
796
+ />
797
+ </div>
798
+
799
+ {/* Honeypot field - hidden from users */}
800
+ <input
801
+ type="text"
802
+ name="hp"
803
+ style={{ display: 'none' }}
804
+ tabIndex={-1}
805
+ autoComplete="off"
806
+ />
807
+
808
+ {/* Turnstile CAPTCHA */}
809
+ <div className="form-group">
810
+ <Turnstile
811
+ sitekey={process.env.REACT_APP_TURNSTILE_SITE_KEY!}
812
+ onVerify={(token) => setTurnstileToken(token)}
813
+ />
814
+ </div>
815
+
816
+ {errorMessage && (
817
+ <div className="error-message">{errorMessage}</div>
818
+ )}
819
+
820
+ <button
821
+ type="submit"
822
+ disabled={status === 'submitting' || !turnstileToken || !csrfToken}
823
+ >
824
+ {status === 'submitting' ? 'Sending...' : 'Send Message'}
825
+ </button>
826
+
827
+ {status === 'success' && (
828
+ <div className="success-message">
829
+ Message sent successfully!
830
+ </div>
831
+ )}
832
+ </form>
833
+ );
834
+ }
835
+ ```
836
+
837
+ ### Vue.js Contact Form
838
+
839
+ ```vue
840
+ <template>
841
+ <form @submit.prevent="submitForm" class="contact-form">
842
+ <div class="form-group">
843
+ <label for="name">Name *</label>
844
+ <input
845
+ v-model="formData.name"
846
+ type="text"
847
+ id="name"
848
+ required
849
+ :disabled="isSubmitting"
850
+ />
851
+ </div>
852
+
853
+ <div class="form-group">
854
+ <label for="email">Email *</label>
855
+ <input
856
+ v-model="formData.email"
857
+ type="email"
858
+ id="email"
859
+ required
860
+ :disabled="isSubmitting"
861
+ />
862
+ </div>
863
+
864
+ <div class="form-group">
865
+ <label for="message">Message *</label>
866
+ <textarea
867
+ v-model="formData.message"
868
+ id="message"
869
+ rows="5"
870
+ required
871
+ :disabled="isSubmitting"
872
+ ></textarea>
873
+ </div>
874
+
875
+ <!-- Honeypot -->
876
+ <input
877
+ type="text"
878
+ name="hp"
879
+ v-model="honeypot"
880
+ style="display: none"
881
+ />
882
+
883
+ <!-- Turnstile -->
884
+ <div ref="turnstileContainer"></div>
885
+
886
+ <div v-if="errorMessage" class="error">
887
+ {{ errorMessage }}
888
+ </div>
889
+
890
+ <button type="submit" :disabled="isSubmitting || !turnstileToken">
891
+ {{ isSubmitting ? 'Sending...' : 'Send Message' }}
892
+ </button>
893
+ </form>
894
+ </template>
895
+
896
+ <script>
897
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
898
+
899
+ export default {
900
+ data() {
901
+ return {
902
+ formData: {
903
+ name: '',
904
+ email: '',
905
+ message: ''
906
+ },
907
+ honeypot: '',
908
+ turnstileToken: '',
909
+ csrfToken: null,
910
+ isSubmitting: false,
911
+ errorMessage: ''
912
+ };
913
+ },
914
+ async mounted() {
915
+ // Fetch CSRF token
916
+ const client = createPerspectApiClient({
917
+ baseUrl: process.env.VUE_APP_API_URL,
918
+ apiKey: process.env.VUE_APP_API_KEY
919
+ });
920
+
921
+ try {
922
+ const response = await client.getCsrfToken('my-site');
923
+ this.csrfToken = response.data.token;
924
+ } catch (error) {
925
+ console.error('Failed to fetch CSRF token:', error);
926
+ }
927
+
928
+ // Initialize Turnstile
929
+ window.turnstile.render(this.$refs.turnstileContainer, {
930
+ sitekey: process.env.VUE_APP_TURNSTILE_SITE_KEY,
931
+ callback: (token) => {
932
+ this.turnstileToken = token;
933
+ }
934
+ });
935
+ },
936
+ methods: {
937
+ async submitForm() {
938
+ if (this.honeypot) return; // Bot detected
939
+
940
+ if (!this.csrfToken) {
941
+ this.errorMessage = 'Security token not loaded. Please refresh.';
942
+ return;
943
+ }
944
+
945
+ this.isSubmitting = true;
946
+ this.errorMessage = '';
947
+
948
+ const client = createPerspectApiClient({
949
+ baseUrl: process.env.VUE_APP_API_URL,
950
+ apiKey: process.env.VUE_APP_API_KEY
951
+ });
952
+
953
+ try {
954
+ await client.contact.submitContact('my-site', {
955
+ ...this.formData,
956
+ turnstileToken: this.turnstileToken
957
+ }, this.csrfToken); // Pass CSRF token
958
+
959
+ // Reset form
960
+ this.formData = {
961
+ name: '',
962
+ email: '',
963
+ message: ''
964
+ };
965
+ window.turnstile.reset();
966
+
967
+ alert('Message sent successfully!');
968
+ } catch (error) {
969
+ this.handleError(error);
970
+ } finally {
971
+ this.isSubmitting = false;
972
+ }
973
+ },
974
+ handleError(error) {
975
+ if (error.status === 429) {
976
+ this.errorMessage = 'Too many attempts. Please wait.';
977
+ } else {
978
+ this.errorMessage = 'Failed to send message. Please try again.';
979
+ }
980
+ }
981
+ }
982
+ };
983
+ </script>
984
+ ```
985
+
986
+ ### Next.js API Route Handler
987
+
988
+ ```typescript
989
+ // pages/api/contact.ts or app/api/contact/route.ts
990
+ import { NextRequest, NextResponse } from 'next/server';
991
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
992
+
993
+ export async function POST(request: NextRequest) {
994
+ const body = await request.json();
995
+
996
+ // Initialize SDK with server-side credentials
997
+ const client = createPerspectApiClient({
998
+ baseUrl: process.env.PERSPECT_API_URL!,
999
+ apiKey: process.env.PERSPECT_API_KEY!
1000
+ });
1001
+
1002
+ try {
1003
+ // Server-side validation
1004
+ if (!body.name || !body.email || !body.message) {
1005
+ return NextResponse.json(
1006
+ { error: 'Missing required fields' },
1007
+ { status: 400 }
1008
+ );
1009
+ }
1010
+
1011
+ // Submit to PerspectAPI
1012
+ const result = await client.contact.submitContact(
1013
+ process.env.SITE_NAME!,
1014
+ {
1015
+ name: body.name,
1016
+ email: body.email,
1017
+ subject: body.subject,
1018
+ message: body.message,
1019
+ phone: body.phone,
1020
+ company: body.company,
1021
+ turnstileToken: body.turnstileToken,
1022
+ metadata: {
1023
+ ip: request.ip,
1024
+ userAgent: request.headers.get('user-agent'),
1025
+ source: 'website'
1026
+ }
1027
+ }
1028
+ );
1029
+
1030
+ return NextResponse.json({
1031
+ success: true,
1032
+ id: result.data.id,
1033
+ message: 'Contact form submitted successfully'
1034
+ });
1035
+ } catch (error: any) {
1036
+ console.error('Contact form error:', error);
1037
+
1038
+ if (error.status === 429) {
1039
+ return NextResponse.json(
1040
+ { error: 'Too many submissions. Please wait.' },
1041
+ { status: 429 }
1042
+ );
1043
+ }
1044
+
1045
+ return NextResponse.json(
1046
+ { error: 'Failed to submit contact form' },
1047
+ { status: 500 }
1048
+ );
1049
+ }
1050
+ }
1051
+ ```
1052
+
1053
+ ### Admin Dashboard Component
1054
+
1055
+ ```typescript
1056
+ // Admin dashboard for managing contact submissions
1057
+ import { useState, useEffect } from 'react';
1058
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
1059
+
1060
+ export function ContactAdmin() {
1061
+ const [submissions, setSubmissions] = useState([]);
1062
+ const [stats, setStats] = useState(null);
1063
+ const [filter, setFilter] = useState('unread');
1064
+
1065
+ const client = createPerspectApiClient({
1066
+ baseUrl: process.env.REACT_APP_API_URL!,
1067
+ jwt: localStorage.getItem('adminToken')!
1068
+ });
1069
+
1070
+ useEffect(() => {
1071
+ loadSubmissions();
1072
+ loadStats();
1073
+ }, [filter]);
1074
+
1075
+ const loadSubmissions = async () => {
1076
+ const result = await client.contact.getContactSubmissions('my-site', {
1077
+ status: filter === 'all' ? undefined : filter,
1078
+ limit: 50,
1079
+ page: 1
1080
+ });
1081
+ setSubmissions(result.data);
1082
+ };
1083
+
1084
+ const loadStats = async () => {
1085
+ const stats = await client.contact.getContactStats('my-site');
1086
+ setStats(stats.data);
1087
+ };
1088
+
1089
+ const markAsRead = async (id: string) => {
1090
+ await client.contact.markContactAsRead('my-site', id);
1091
+ await loadSubmissions();
1092
+ };
1093
+
1094
+ const archiveSubmission = async (id: string) => {
1095
+ await client.contact.archiveContact('my-site', id);
1096
+ await loadSubmissions();
1097
+ };
1098
+
1099
+ const exportSubmissions = async () => {
1100
+ const result = await client.contact.exportContacts('my-site', {
1101
+ format: 'csv',
1102
+ status: filter === 'all' ? undefined : filter
1103
+ });
1104
+
1105
+ // Download the export
1106
+ window.open(result.data.downloadUrl, '_blank');
1107
+ };
1108
+
1109
+ const bulkMarkRead = async () => {
1110
+ const unreadIds = submissions
1111
+ .filter(s => s.status === 'unread')
1112
+ .map(s => s.id);
1113
+
1114
+ if (unreadIds.length > 0) {
1115
+ await client.contact.bulkUpdateContacts('my-site', {
1116
+ ids: unreadIds,
1117
+ action: 'mark_read'
1118
+ });
1119
+ await loadSubmissions();
1120
+ }
1121
+ };
1122
+
1123
+ return (
1124
+ <div className="contact-admin">
1125
+ <div className="stats">
1126
+ {stats && (
1127
+ <>
1128
+ <div>Total: {stats.totalSubmissions}</div>
1129
+ <div>Unread: {stats.unreadSubmissions}</div>
1130
+ <div>Archived: {stats.archivedSubmissions}</div>
1131
+ </>
1132
+ )}
1133
+ </div>
1134
+
1135
+ <div className="actions">
1136
+ <select value={filter} onChange={(e) => setFilter(e.target.value)}>
1137
+ <option value="all">All</option>
1138
+ <option value="unread">Unread</option>
1139
+ <option value="read">Read</option>
1140
+ <option value="archived">Archived</option>
1141
+ </select>
1142
+
1143
+ <button onClick={bulkMarkRead}>Mark All Read</button>
1144
+ <button onClick={exportSubmissions}>Export CSV</button>
1145
+ </div>
1146
+
1147
+ <div className="submissions">
1148
+ {submissions.map(submission => (
1149
+ <div key={submission.id} className="submission-card">
1150
+ <div className="header">
1151
+ <strong>{submission.name}</strong>
1152
+ <span>{submission.email}</span>
1153
+ <span>{new Date(submission.createdAt).toLocaleDateString()}</span>
1154
+ </div>
1155
+
1156
+ <div className="subject">{submission.subject}</div>
1157
+ <div className="message">{submission.message}</div>
1158
+
1159
+ <div className="actions">
1160
+ {submission.status === 'unread' && (
1161
+ <button onClick={() => markAsRead(submission.id)}>
1162
+ Mark Read
1163
+ </button>
1164
+ )}
1165
+ <button onClick={() => archiveSubmission(submission.id)}>
1166
+ Archive
1167
+ </button>
1168
+ </div>
1169
+ </div>
1170
+ ))}
1171
+ </div>
1172
+ </div>
1173
+ );
1174
+ }
1175
+ ```
1176
+
1177
+ ### Cloudflare Worker for Contact Processing
1178
+
1179
+ ```typescript
1180
+ // Cloudflare Worker to handle contact forms
1181
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
1182
+
1183
+ export default {
1184
+ async fetch(request: Request, env: Env): Promise<Response> {
1185
+ if (request.method !== 'POST') {
1186
+ return new Response('Method not allowed', { status: 405 });
1187
+ }
1188
+
1189
+ const client = createPerspectApiClient({
1190
+ baseUrl: env.API_URL,
1191
+ apiKey: env.API_KEY
1192
+ });
1193
+
1194
+ try {
1195
+ const formData = await request.json();
1196
+
1197
+ // Get client IP for rate limiting
1198
+ const clientIp = request.headers.get('CF-Connecting-IP');
1199
+
1200
+ const result = await client.contact.submitContact(env.SITE_NAME, {
1201
+ ...formData,
1202
+ metadata: {
1203
+ ip: clientIp,
1204
+ country: request.cf?.country,
1205
+ source: 'cloudflare-worker'
1206
+ }
1207
+ });
1208
+
1209
+ // Send email notification (optional)
1210
+ await env.EMAIL_QUEUE.send({
1211
+ to: env.ADMIN_EMAIL,
1212
+ subject: `New Contact: ${formData.subject}`,
1213
+ text: `New contact form submission from ${formData.name}`
1214
+ });
1215
+
1216
+ return new Response(JSON.stringify({
1217
+ success: true,
1218
+ id: result.data.id
1219
+ }), {
1220
+ headers: { 'Content-Type': 'application/json' }
1221
+ });
1222
+ } catch (error: any) {
1223
+ return new Response(JSON.stringify({
1224
+ error: error.message
1225
+ }), {
1226
+ status: error.status || 500,
1227
+ headers: { 'Content-Type': 'application/json' }
1228
+ });
1229
+ }
1230
+ }
1231
+ };
1232
+ ```
1233
+
1234
+ ## Error Handling
1235
+
1236
+ ```typescript
1237
+ import { createApiError } from 'perspectapi-ts-sdk';
1238
+
1239
+ try {
1240
+ await client.contact.submitContact('my-site', data);
1241
+ } catch (error) {
1242
+ const apiError = createApiError(error);
1243
+
1244
+ switch (apiError.status) {
1245
+ case 400:
1246
+ // Validation error
1247
+ console.error('Invalid input:', apiError.details);
1248
+ break;
1249
+ case 429:
1250
+ // Rate limited
1251
+ console.warn('Too many requests, please wait');
1252
+ break;
1253
+ case 500:
1254
+ // Server error
1255
+ console.error('Server error, please try again');
1256
+ break;
1257
+ default:
1258
+ console.error('Unexpected error:', apiError.message);
1259
+ }
1260
+ }
1261
+ ```
1262
+
1263
+ ## Rate Limiting
1264
+
1265
+ The API enforces rate limits to prevent abuse:
1266
+
1267
+ - **Default**: 5 submissions per 15 minutes per IP/email combination
1268
+ - **Response**: HTTP 429 when limit exceeded
1269
+ - **Reset**: Limits reset after the time window expires
1270
+
1271
+ Handle rate limits gracefully:
1272
+
1273
+ ```typescript
1274
+ async function submitWithRetry(data: any, maxRetries = 3) {
1275
+ let attempts = 0;
1276
+
1277
+ while (attempts < maxRetries) {
1278
+ try {
1279
+ return await client.contact.submitContact('my-site', data);
1280
+ } catch (error) {
1281
+ if (error.status === 429 && attempts < maxRetries - 1) {
1282
+ // Wait before retry (exponential backoff)
1283
+ const delay = Math.pow(2, attempts) * 1000;
1284
+ await new Promise(resolve => setTimeout(resolve, delay));
1285
+ attempts++;
1286
+ } else {
1287
+ throw error;
1288
+ }
1289
+ }
1290
+ }
1291
+ }
1292
+ ```
1293
+
1294
+ ## Testing
1295
+
1296
+ ```typescript
1297
+ // Jest test example
1298
+ describe('Contact Form', () => {
1299
+ const client = createPerspectApiClient({
1300
+ baseUrl: 'http://localhost:8787',
1301
+ apiKey: 'test-key'
1302
+ });
1303
+
1304
+ test('should submit contact form', async () => {
1305
+ const result = await client.contact.submitContact('test-site', {
1306
+ name: 'Test User',
1307
+ email: 'test@example.com',
1308
+ message: 'This is a test message'
1309
+ });
1310
+
1311
+ expect(result.data.id).toBeDefined();
1312
+ expect(result.data.status).toBe('pending');
1313
+ });
1314
+
1315
+ test('should handle rate limiting', async () => {
1316
+ // Submit multiple times to trigger rate limit
1317
+ for (let i = 0; i < 5; i++) {
1318
+ await client.contact.submitContact('test-site', {
1319
+ name: 'Test User',
1320
+ email: 'test@example.com',
1321
+ message: `Message ${i}`
1322
+ });
1323
+ }
1324
+
1325
+ // 6th submission should fail
1326
+ await expect(
1327
+ client.contact.submitContact('test-site', {
1328
+ name: 'Test User',
1329
+ email: 'test@example.com',
1330
+ message: 'This should fail'
1331
+ })
1332
+ ).rejects.toThrow('Too many requests');
1333
+ });
1334
+ });
1335
+ ```
1336
+
1337
+ ## Migration from Other Systems
1338
+
1339
+ If migrating from another contact form system:
1340
+
1341
+ ```typescript
1342
+ // Example: Migrate from WordPress Contact Form 7
1343
+ async function migrateContacts(oldContacts: any[]) {
1344
+ const client = createPerspectApiClient({
1345
+ baseUrl: process.env.API_URL!,
1346
+ apiKey: process.env.API_KEY!
1347
+ });
1348
+
1349
+ for (const contact of oldContacts) {
1350
+ // Transform old format to new
1351
+ const submission = {
1352
+ name: contact.your_name,
1353
+ email: contact.your_email,
1354
+ subject: contact.your_subject,
1355
+ message: contact.your_message,
1356
+ metadata: {
1357
+ migrated: true,
1358
+ originalId: contact.id,
1359
+ originalDate: contact.date
1360
+ }
1361
+ };
1362
+
1363
+ try {
1364
+ await client.contact.submitContact('my-site', submission);
1365
+ console.log(`Migrated contact ${contact.id}`);
1366
+ } catch (error) {
1367
+ console.error(`Failed to migrate ${contact.id}:`, error);
1368
+ }
1369
+
1370
+ // Rate limit prevention
1371
+ await new Promise(resolve => setTimeout(resolve, 100));
1372
+ }
1373
+ }
1374
+ ```
1375
+
1376
+ ## Security Best Practices
1377
+
1378
+ 1. **Always use HTTPS** for API calls
1379
+ 2. **Implement Turnstile** on public forms
1380
+ 3. **Validate input** on both client and server
1381
+ 4. **Use honeypot fields** for additional bot protection
1382
+ 5. **Monitor rate limits** and adjust as needed
1383
+ 6. **Sanitize output** when displaying submissions
1384
+ 7. **Implement access controls** for admin functions
1385
+ 8. **Log all submissions** for audit purposes
1386
+ 9. **Regular backups** of contact data
1387
+ 10. **GDPR compliance** - provide data export/deletion
1388
+
1389
+ ## Compliance
1390
+
1391
+ The contact system helps with compliance:
1392
+
1393
+ - **GDPR**: Data export, deletion capabilities
1394
+ - **CAN-SPAM**: Clear identification, no deceptive practices
1395
+ - **Privacy**: IP addresses stored for security, deletable on request
1396
+ - **Accessibility**: Form fields properly labeled for screen readers