@djangocfg/layouts 1.2.32 → 1.2.34

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,181 @@
1
+ /**
2
+ * ValidationErrorToast Component
3
+ *
4
+ * Custom toast for validation errors with copy button
5
+ * Formats Zod validation errors and provides one-click copy functionality
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import React from 'react';
11
+ import { ToastAction } from '@djangocfg/ui';
12
+ import type { ZodError } from 'zod';
13
+ import { ValidationErrorButtons } from './ValidationErrorButtons';
14
+
15
+ export interface ValidationErrorDetail {
16
+ operation: string;
17
+ path: string;
18
+ method: string;
19
+ error: ZodError;
20
+ response: any;
21
+ timestamp: Date;
22
+ }
23
+
24
+ export interface ValidationErrorToastConfig {
25
+ /** Show operation name in title */
26
+ showOperation?: boolean;
27
+ /** Show API path in description */
28
+ showPath?: boolean;
29
+ /** Show error count in description */
30
+ showErrorCount?: boolean;
31
+ /** Maximum number of issues to show in description */
32
+ maxIssuesInDescription?: number;
33
+ /** Custom title prefix */
34
+ titlePrefix?: string;
35
+ }
36
+
37
+ const defaultConfig: ValidationErrorToastConfig = {
38
+ showOperation: true,
39
+ showPath: true,
40
+ showErrorCount: true,
41
+ maxIssuesInDescription: 3,
42
+ titlePrefix: '❌ Validation Error',
43
+ };
44
+
45
+ /**
46
+ * Format Zod error issues for display in toast description
47
+ */
48
+ export function formatZodIssuesForToast(
49
+ error: ZodError,
50
+ maxIssues: number = 3
51
+ ): string {
52
+ const issues = error.issues.slice(0, maxIssues);
53
+ const formatted = issues.map((issue) => {
54
+ const path = issue.path.join('.') || 'root';
55
+ return `${path}: ${issue.message}`;
56
+ });
57
+
58
+ if (error.issues.length > maxIssues) {
59
+ formatted.push(`... and ${error.issues.length - maxIssues} more`);
60
+ }
61
+
62
+ return formatted.join(', ');
63
+ }
64
+
65
+ /**
66
+ * Format full error details for copying to clipboard
67
+ * Includes all error information in structured JSON format
68
+ */
69
+ export function formatErrorForClipboard(detail: ValidationErrorDetail): string {
70
+ const errorData = {
71
+ timestamp: detail.timestamp.toISOString(),
72
+ operation: detail.operation,
73
+ endpoint: {
74
+ method: detail.method,
75
+ path: detail.path,
76
+ },
77
+ validation_errors: detail.error.issues.map((issue) => ({
78
+ path: issue.path.join('.') || 'root',
79
+ message: issue.message,
80
+ code: issue.code,
81
+ ...(('expected' in issue) && { expected: issue.expected }),
82
+ ...(('received' in issue) && { received: issue.received }),
83
+ ...(('minimum' in issue) && { minimum: issue.minimum }),
84
+ ...(('maximum' in issue) && { maximum: issue.maximum }),
85
+ })),
86
+ response: detail.response,
87
+ total_errors: detail.error.issues.length,
88
+ };
89
+
90
+ return JSON.stringify(errorData, null, 2);
91
+ }
92
+
93
+ /**
94
+ * Build toast title from validation error
95
+ */
96
+ export function buildToastTitle(
97
+ detail: ValidationErrorDetail,
98
+ config: ValidationErrorToastConfig = defaultConfig
99
+ ): string {
100
+ const titleParts: string[] = [config.titlePrefix || '❌ Validation Error'];
101
+
102
+ if (config.showOperation) {
103
+ titleParts.push(`in ${detail.operation}`);
104
+ }
105
+
106
+ return titleParts.join(' ');
107
+ }
108
+
109
+ /**
110
+ * Build toast description from validation error
111
+ */
112
+ export function buildToastDescription(
113
+ detail: ValidationErrorDetail,
114
+ config: ValidationErrorToastConfig = defaultConfig
115
+ ): string {
116
+ const descriptionParts: string[] = [];
117
+
118
+ // Add HTTP method and path
119
+ if (config.showPath) {
120
+ descriptionParts.push(`${detail.method} ${detail.path}`);
121
+ }
122
+
123
+ // Add error count
124
+ if (config.showErrorCount) {
125
+ const count = detail.error.issues.length;
126
+ const plural = count === 1 ? 'error' : 'errors';
127
+ descriptionParts.push(`${count} ${plural}`);
128
+ }
129
+
130
+ // Add formatted error messages
131
+ descriptionParts.push(
132
+ formatZodIssuesForToast(
133
+ detail.error,
134
+ config.maxIssuesInDescription
135
+ )
136
+ );
137
+
138
+ return descriptionParts.join(' • ');
139
+ }
140
+
141
+ /**
142
+ * Create complete toast options for validation error
143
+ *
144
+ * Usage:
145
+ * ```typescript
146
+ * const { toast } = useToast();
147
+ *
148
+ * const toastOptions = createValidationErrorToast(errorDetail, {
149
+ * onCopySuccess: () => toast({ title: '✅ Copied!' }),
150
+ * onCopyError: () => toast({ title: '❌ Copy failed', variant: 'destructive' }),
151
+ * });
152
+ *
153
+ * toast(toastOptions);
154
+ * ```
155
+ */
156
+ export function createValidationErrorToast(
157
+ detail: ValidationErrorDetail,
158
+ options?: {
159
+ config?: Partial<ValidationErrorToastConfig>;
160
+ duration?: number;
161
+ onCopySuccess?: () => void;
162
+ onCopyError?: (error: Error) => void;
163
+ }
164
+ ) {
165
+ const config: ValidationErrorToastConfig = {
166
+ ...defaultConfig,
167
+ ...options?.config,
168
+ };
169
+
170
+ return {
171
+ title: buildToastTitle(detail, config),
172
+ description: (
173
+ <div className="flex flex-col gap-2">
174
+ <div>{buildToastDescription(detail, config)}</div>
175
+ <ValidationErrorButtons detail={detail} />
176
+ </div>
177
+ ),
178
+ variant: 'destructive' as const,
179
+ duration: options?.duration,
180
+ };
181
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * cURL Generator
3
+ *
4
+ * Generates cURL commands from API request details with authentication token
5
+ */
6
+
7
+ export interface CurlOptions {
8
+ method: string;
9
+ path: string;
10
+ token?: string;
11
+ body?: any;
12
+ headers?: Record<string, string>;
13
+ baseUrl?: string;
14
+ queryParams?: Record<string, string>;
15
+ }
16
+
17
+ /**
18
+ * Get authentication token from localStorage
19
+ */
20
+ export function getAuthToken(): string | null {
21
+ if (typeof window === 'undefined') return null;
22
+
23
+ try {
24
+ // Priority order: access_token > token > auth_token
25
+ const token = localStorage.getItem('access_token') ||
26
+ localStorage.getItem('token') ||
27
+ localStorage.getItem('auth_token');
28
+
29
+ return token;
30
+ } catch (error) {
31
+ console.error('Failed to get auth token:', error);
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Format headers for cURL command
38
+ */
39
+ function formatHeaders(headers: Record<string, string>): string[] {
40
+ return Object.entries(headers).map(
41
+ ([key, value]) => `-H '${key}: ${value}'`
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Escape single quotes in string for shell
47
+ */
48
+ function escapeShell(str: string): string {
49
+ return str.replace(/'/g, "'\\''");
50
+ }
51
+
52
+ /**
53
+ * Generate cURL command from request details
54
+ */
55
+ export function generateCurl(options: CurlOptions): string {
56
+ const {
57
+ method,
58
+ path,
59
+ token = getAuthToken() || undefined, // Auto-fetch if not provided
60
+ body,
61
+ headers = {},
62
+ baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
63
+ } = options;
64
+
65
+ const curlParts: string[] = ['curl'];
66
+
67
+ // Build URL
68
+ const url = `${baseUrl}${path}`;
69
+ curlParts.push(`'${url}'`);
70
+
71
+ // Add method if not GET
72
+ if (method.toUpperCase() !== 'GET') {
73
+ curlParts.push(`-X ${method.toUpperCase()}`);
74
+ }
75
+
76
+ // Default headers
77
+ const allHeaders: Record<string, string> = {
78
+ 'Accept': '*/*',
79
+ 'Content-Type': 'application/json',
80
+ ...headers,
81
+ };
82
+
83
+ // Add Authorization header if token exists
84
+ if (token) {
85
+ allHeaders['Authorization'] = `Bearer ${token}`;
86
+ }
87
+
88
+ // Add all headers
89
+ const headerStrings = formatHeaders(allHeaders);
90
+ curlParts.push(...headerStrings);
91
+
92
+ // Add body for non-GET requests
93
+ if (body && method.toUpperCase() !== 'GET') {
94
+ const bodyJson = typeof body === 'string'
95
+ ? body
96
+ : JSON.stringify(body, null, 2);
97
+ curlParts.push(`-d '${escapeShell(bodyJson)}'`);
98
+ }
99
+
100
+ // Join with line continuation
101
+ return curlParts.join(' \\\n ');
102
+ }
103
+
104
+ /**
105
+ * Generate cURL from validation error details
106
+ * Auto-fetches token from localStorage
107
+ */
108
+ export function generateCurlFromError(detail: {
109
+ method: string;
110
+ path: string;
111
+ response?: any;
112
+ }): string {
113
+ return generateCurl({
114
+ method: detail.method,
115
+ path: detail.path,
116
+ // token is auto-fetched in generateCurl
117
+ });
118
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Validation Error Tracking
3
+ *
4
+ * Automatic capture and management of Zod validation errors
5
+ * from API client using browser CustomEvent API.
6
+ */
7
+
8
+ export {
9
+ ValidationErrorProvider,
10
+ useValidationErrors,
11
+ type ValidationErrorDetail,
12
+ type StoredValidationError,
13
+ type ValidationErrorConfig,
14
+ type ValidationErrorContextValue,
15
+ } from './ValidationErrorContext';
16
+
17
+ export {
18
+ createValidationErrorToast,
19
+ buildToastTitle,
20
+ buildToastDescription,
21
+ formatZodIssuesForToast,
22
+ formatErrorForClipboard,
23
+ type ValidationErrorToastConfig,
24
+ } from './ValidationErrorToast';
25
+
26
+ export {
27
+ ValidationErrorButtons,
28
+ type ValidationErrorButtonsProps,
29
+ } from './ValidationErrorButtons';
30
+
31
+ export {
32
+ generateCurl,
33
+ generateCurlFromError,
34
+ getAuthToken,
35
+ type CurlOptions,
36
+ } from './curl-generator';