@adlas/create-app 1.0.11 → 1.0.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adlas/create-app",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Adlas project initializer with Figma and Swagger integration",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",
@@ -0,0 +1,3 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M4 6L8 10L12 6" stroke="#000" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
3
+ </svg>
@@ -0,0 +1,8 @@
1
+ import type { SVGProps } from 'react';
2
+
3
+ const ChevronDownIcon = (props: SVGProps<SVGSVGElement>) => (
4
+ <svg viewBox="0 0 16 16" fill="none" stroke="foreground" width="1em" height="1em" {...props}>
5
+ <path strokeLinecap="round" strokeLinejoin="round" d="m4 6 4 4 4-4" />
6
+ </svg>
7
+ );
8
+ export default ChevronDownIcon;
@@ -0,0 +1 @@
1
+ export { default as ChevronDownIcon } from './ChevronDownIcon';
@@ -15,8 +15,6 @@ import type { FormFieldValidation } from '@/validations';
15
15
 
16
16
  import { createZodErrorMap } from '@/validations/zodErrorMap';
17
17
 
18
- import type { FileValidationError } from '../DocumentUpload';
19
-
20
18
  import { FormContext } from './FormContext';
21
19
 
22
20
  /**
@@ -87,7 +85,7 @@ type FormProps<T extends FieldValues> = {
87
85
 
88
86
  export type FieldEvent = {
89
87
  target?: {
90
- value?: string | File | FileValidationError | null;
88
+ value?: string | File | null;
91
89
  };
92
90
  };
93
91
 
@@ -4,7 +4,6 @@ export * from './Button';
4
4
  export * from './Checkbox';
5
5
  export * from './Chip';
6
6
  export * from './DatePicker';
7
- export * from './DocumentUpload';
8
7
  export * from './form/Form';
9
8
  export * from './form/FormContext';
10
9
  export * from './form/FormGenerator';
@@ -2,8 +2,6 @@ import type { useTranslations } from 'next-intl';
2
2
 
3
3
  import { z } from 'zod';
4
4
 
5
- import type { FileValidationError } from '@/components/ui';
6
-
7
5
  // ==================== Types ====================
8
6
 
9
7
  /**
@@ -176,22 +174,10 @@ export function optional<T extends z.ZodTypeAny>(schema: T) {
176
174
  return schema.optional().or(z.literal(''));
177
175
  }
178
176
 
179
- /**
180
- * Type guard for file validation errors
181
- */
182
- const isFileValidationError = (value: unknown): value is FileValidationError => {
183
- return (
184
- typeof value === 'object' &&
185
- value !== null &&
186
- 'isValidationError' in value &&
187
- value.isValidationError === true
188
- );
189
- };
190
-
191
177
  /**
192
178
  * Valid file input types for validation
193
179
  */
194
- type FileInput = File | string | FileValidationError | null | undefined;
180
+ type FileInput = File | string | null | undefined;
195
181
 
196
182
  /**
197
183
  * Creates a file validation schema with i18n support
@@ -200,13 +186,6 @@ type FileInput = File | string | FileValidationError | null | undefined;
200
186
  export const createFileValidation = (required = false) =>
201
187
  z.custom<FileInput>().superRefine((value, ctx) => {
202
188
  // Handle validation error objects
203
- if (isFileValidationError(value)) {
204
- ctx.addIssue({
205
- code: 'custom',
206
- message: value.errorMessage,
207
- });
208
- return;
209
- }
210
189
 
211
190
  // If required, reject null/undefined/empty string
212
191
  if (required && !value) {
@@ -1,415 +0,0 @@
1
- 'use client';
2
-
3
- import type { ChangeEvent, DragEvent, KeyboardEvent } from 'react';
4
-
5
- import { memo, useCallback, useEffect, useRef, useState } from 'react';
6
-
7
- import { Spinner } from '@heroui/react';
8
- import clsx from 'clsx';
9
- import { useLocale, useTranslations } from 'next-intl';
10
- import Image from 'next/image';
11
-
12
- import { uploadImageToSaleor } from '@/services/user';
13
- import {
14
- compressImage,
15
- FILE_CONFIG,
16
- formatFileSize,
17
- getAllowedExtensions,
18
- getFileInfoText,
19
- } from '@/utils/file';
20
- import { getOptimalCompressionOptions } from '@/utils/file/imageCompression';
21
-
22
- import { Button } from './Button';
23
- import { Icon } from './Icon';
24
-
25
- // ============================================================================
26
- // Types
27
- // ============================================================================
28
-
29
- export type FileValidationError = {
30
- isValidationError: true;
31
- errorMessage: string;
32
- };
33
-
34
- type DocumentUploadProps = {
35
- label: string;
36
- description?: string;
37
- onChange: (file: File | FileValidationError | null) => void;
38
- onImageUploaded?: (imageId: string, imageUrl: string) => void;
39
- onError?: (error: string) => void;
40
- className?: string;
41
- value?: string | null;
42
- hasError?: boolean;
43
- errorMessage?: string;
44
- accept?: string;
45
- required?: boolean;
46
- maxSize?: number;
47
- /** If true, file will not be uploaded immediately - parent handles upload */
48
- deferUpload?: boolean;
49
- };
50
-
51
- // ============================================================================
52
- // Constants
53
- // ============================================================================
54
-
55
- const CONTAINER_HEIGHT = 'h-40';
56
- const ICON_SIZE = 'size-5';
57
-
58
- // ============================================================================
59
- // Helper Functions
60
- // ============================================================================
61
-
62
- /**
63
- * Validates a file against size and type constraints
64
- */
65
- function validateFile(
66
- file: File,
67
- accept: string,
68
- maxFileSize: number,
69
- t: ReturnType<typeof useTranslations>,
70
- locale: 'en' | 'de',
71
- ): string | null {
72
- // Check file size
73
- if (file.size > maxFileSize) {
74
- return t('fileSizeError', { maxSize: formatFileSize(maxFileSize, locale) });
75
- }
76
-
77
- // Check file type
78
- const allowedExtensions = getAllowedExtensions(accept);
79
- const fileExtension = file.name.split('.').pop()?.toUpperCase();
80
-
81
- if (fileExtension && !allowedExtensions.includes(fileExtension)) {
82
- return t('fileTypeError');
83
- }
84
-
85
- return null;
86
- }
87
-
88
- /**
89
- * Cleans up blob URL if it exists
90
- */
91
- function revokeObjectURL(url: string | null) {
92
- if (url && url.startsWith('blob:')) {
93
- URL.revokeObjectURL(url);
94
- }
95
- }
96
-
97
- // ============================================================================
98
- // Component
99
- // ============================================================================
100
-
101
- function DocumentUploadComponent({
102
- label,
103
- onChange,
104
- onImageUploaded,
105
- onError,
106
- className,
107
- errorMessage,
108
- value,
109
- hasError,
110
- accept = 'image/*,.pdf',
111
- maxSize,
112
- deferUpload = false,
113
- }: DocumentUploadProps) {
114
- const t = useTranslations('FileUpload');
115
- const locale = useLocale() as 'en' | 'de';
116
- const [previewUrl, setPreviewUrl] = useState<string | null>(value || null);
117
- const [isDragOver, setIsDragOver] = useState(false);
118
- const [validationError, setValidationError] = useState<string | null>(null);
119
- const [isCompressing, setIsCompressing] = useState(false);
120
- const [isUploading, setIsUploading] = useState(false);
121
- const inputRef = useRef<HTMLInputElement | null>(null);
122
-
123
- const maxFileSize = maxSize || FILE_CONFIG.MAX_FILE_SIZE;
124
-
125
- // ============================================================================
126
- // Effects
127
- // ============================================================================
128
-
129
- // Sync previewUrl with value prop from form
130
- useEffect(() => {
131
- if (value && typeof value === 'string') {
132
- setPreviewUrl(value);
133
- }
134
- }, [value]);
135
-
136
- // Cleanup object URLs on unmount or when preview changes
137
- useEffect(() => {
138
- return () => {
139
- revokeObjectURL(previewUrl);
140
- };
141
- }, [previewUrl]);
142
-
143
- // ============================================================================
144
- // Handlers
145
- // ============================================================================
146
-
147
- const handleFileChange = useCallback(
148
- async (file: File | null) => {
149
- setValidationError(null);
150
-
151
- if (file) {
152
- try {
153
- let processedFile = file;
154
-
155
- // Compress image files before validation
156
- if (file.type.startsWith('image/')) {
157
- setIsCompressing(true);
158
- const compressionOptions = getOptimalCompressionOptions(file);
159
-
160
- processedFile = await compressImage(file, compressionOptions);
161
- setIsCompressing(false);
162
- }
163
-
164
- const error = validateFile(processedFile, accept, maxFileSize, t, locale);
165
-
166
- if (error) {
167
- setValidationError(error);
168
- setPreviewUrl(null);
169
- onChange({
170
- isValidationError: true,
171
- errorMessage: error,
172
- });
173
- if (inputRef.current) {
174
- inputRef.current.value = '';
175
- }
176
-
177
- return;
178
- }
179
-
180
- // Set preview immediately for better UX
181
- const objectUrl = URL.createObjectURL(processedFile);
182
- setPreviewUrl(objectUrl);
183
- onChange(processedFile);
184
-
185
- // Upload to Saleor if it's an image and deferUpload is false
186
- if (!deferUpload && processedFile.type.startsWith('image/') && onImageUploaded) {
187
- setIsUploading(true);
188
-
189
- try {
190
- const result = await uploadImageToSaleor(processedFile);
191
-
192
- if (result.success && result.imageId && result.imageUrl) {
193
- // Notify parent component with the uploaded image URL
194
- onImageUploaded(result.imageId, result.imageUrl);
195
- // Revoke the blob URL before setting the new URL
196
- revokeObjectURL(previewUrl);
197
- setPreviewUrl(result.imageUrl);
198
- } else {
199
- console.error('❌ Image upload failed:', result.error);
200
- onError?.(result.error || t('uploadFailed'));
201
- setValidationError(result.error || t('uploadFailed'));
202
- }
203
- } catch (uploadError) {
204
- console.error('❌ Image upload error:', uploadError);
205
- const errorMsg =
206
- uploadError instanceof Error ? uploadError.message : t('uploadFailed');
207
-
208
- onError?.(errorMsg);
209
- setValidationError(errorMsg);
210
- } finally {
211
- setIsUploading(false);
212
- }
213
- }
214
- } catch (compressionError) {
215
- console.error('Image compression failed:', compressionError);
216
- const errorMsg = t('compressionError');
217
-
218
- setValidationError(errorMsg);
219
- setPreviewUrl(null);
220
- onChange({
221
- isValidationError: true,
222
- errorMessage: errorMsg,
223
- });
224
- if (inputRef.current) {
225
- inputRef.current.value = '';
226
- }
227
- setIsCompressing(false);
228
- }
229
- } else {
230
- onChange(file);
231
- setPreviewUrl(file);
232
- }
233
- },
234
- [accept, maxFileSize, onChange, onError, onImageUploaded, previewUrl, t, locale, deferUpload],
235
- );
236
-
237
- const handleChange = useCallback(
238
- (e: ChangeEvent<HTMLInputElement>) => {
239
- const file = e.target.files?.[0] || null;
240
- handleFileChange(file);
241
- },
242
- [handleFileChange],
243
- );
244
-
245
- const handleDragOver = useCallback(
246
- (e: DragEvent<HTMLDivElement>) => {
247
- e.preventDefault();
248
- if (!isCompressing && !isUploading) {
249
- setIsDragOver(true);
250
- }
251
- },
252
- [isCompressing, isUploading],
253
- );
254
-
255
- const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {
256
- e.preventDefault();
257
- setIsDragOver(false);
258
- }, []);
259
-
260
- const handleDrop = useCallback(
261
- (e: DragEvent<HTMLDivElement>) => {
262
- e.preventDefault();
263
- setIsDragOver(false);
264
-
265
- if (isCompressing || isUploading) {
266
- return;
267
- }
268
-
269
- const files = e.dataTransfer.files;
270
-
271
- if (files.length > 0) {
272
- handleFileChange(files[0] || null);
273
- }
274
- },
275
- [handleFileChange, isCompressing, isUploading],
276
- );
277
-
278
- const handleTriggerInput = useCallback(() => {
279
- if (!isCompressing && !isUploading) {
280
- inputRef.current?.click();
281
- }
282
- }, [isCompressing, isUploading]);
283
-
284
- const handleRemoveFile = useCallback(() => {
285
- setValidationError(null);
286
- if (inputRef.current) {
287
- inputRef.current.value = '';
288
- }
289
- handleTriggerInput();
290
- }, [handleTriggerInput]);
291
-
292
- const handleKeyDown = useCallback(
293
- (e: KeyboardEvent<HTMLDivElement>) => {
294
- if (e.key === 'Enter' || e.key === ' ') {
295
- e.preventDefault();
296
- handleTriggerInput();
297
- }
298
- },
299
- [handleTriggerInput],
300
- );
301
-
302
- // ============================================================================
303
- // Render
304
- // ============================================================================
305
-
306
- return (
307
- <div
308
- className={clsx(
309
- 'relative flex w-full cursor-pointer flex-col items-center justify-center overflow-hidden border border-dashed border-default-200 p-4 transition-all duration-200 hover:border-default-500',
310
- CONTAINER_HEIGHT,
311
- {
312
- 'border-danger!': hasError || validationError,
313
- 'border-default-500! bg-default-50!': isDragOver,
314
- 'border-transparent!': previewUrl,
315
- 'hover:border-default-300':
316
- !hasError && !isDragOver && !validationError && !isCompressing && !isUploading,
317
- 'cursor-not-allowed! opacity-60': isCompressing || isUploading,
318
- },
319
- className,
320
- )}
321
- role="button"
322
- tabIndex={0}
323
- onClick={handleTriggerInput}
324
- onDragLeave={handleDragLeave}
325
- onDragOver={handleDragOver}
326
- onDrop={handleDrop}
327
- onKeyDown={handleKeyDown}
328
- >
329
- {/* Background Image */}
330
- {previewUrl && (
331
- <>
332
- <Image fill alt="Preview" className="object-cover" src={previewUrl} />
333
- {/* Dark Overlay */}
334
- <div className="absolute inset-0 bg-black/30" />
335
- </>
336
- )}
337
-
338
- {/* Content */}
339
- <div className="relative z-10 flex flex-col items-center justify-center">
340
- <div className="flex items-center justify-center">
341
- {isCompressing || isUploading ? (
342
- <Spinner color="white" />
343
- ) : (
344
- !previewUrl && (
345
- <Icon
346
- className={clsx(ICON_SIZE, 'stroke-default-700 stroke-1.5', {
347
- 'stroke-danger!': hasError || validationError,
348
- })}
349
- name="UploadIcon"
350
- />
351
- )
352
- )}
353
- </div>
354
- <input
355
- ref={inputRef}
356
- accept={accept}
357
- className="hidden"
358
- type="file"
359
- onChange={handleChange}
360
- />
361
- <div className="mt-2 flex flex-col items-center justify-center text-center">
362
- {isCompressing || isUploading ? null : (
363
- <span
364
- className={clsx('text-base leading-6 font-semibold', {
365
- 'text-white': previewUrl,
366
- 'text-default-800': !previewUrl,
367
- })}
368
- >
369
- {label}
370
- </span>
371
- )}
372
- {(hasError || validationError) && (
373
- <span className="mt-0.5 text-sm leading-5 font-medium text-danger">
374
- {validationError || errorMessage}
375
- </span>
376
- )}
377
- {!hasError && !validationError && (
378
- <p
379
- className={clsx('mt-0.5 text-sm leading-5 font-medium', {
380
- 'text-white': previewUrl,
381
- 'text-default-600': !previewUrl,
382
- })}
383
- >
384
- {isCompressing
385
- ? t('compressing')
386
- : isUploading
387
- ? t('uploading')
388
- : previewUrl
389
- ? null
390
- : t('clickOrDrag')}
391
- </p>
392
- )}
393
- {!previewUrl && (
394
- <p className="mt-3 text-xs leading-4 font-medium text-default-500">
395
- {getFileInfoText(accept, maxFileSize, locale, t('format'), t('max'))}
396
- </p>
397
- )}
398
- {previewUrl && !isCompressing && !isUploading && (
399
- <Button
400
- className="mt-2 bg-white text-primary"
401
- size="sm"
402
- variant="solid"
403
- onClick={e => e.stopPropagation()}
404
- onPress={handleRemoveFile}
405
- >
406
- {t('changeImage')}
407
- </Button>
408
- )}
409
- </div>
410
- </div>
411
- </div>
412
- );
413
- }
414
-
415
- export const DocumentUpload = memo(DocumentUploadComponent);