valtech-components 2.0.681 → 2.0.682
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/esm2022/lib/components/molecules/image-crop/image-crop.component.mjs +174 -0
- package/esm2022/lib/components/molecules/image-crop/index.mjs +2 -0
- package/esm2022/lib/components/organisms/avatar-upload/avatar-upload.component.mjs +345 -0
- package/esm2022/lib/components/organisms/avatar-upload/types.mjs +15 -0
- package/esm2022/lib/components/templates/docs-page/docs-page.component.mjs +35 -4
- package/esm2022/lib/components/templates/docs-page/types.mjs +1 -1
- package/esm2022/lib/services/auth/auth.service.mjs +11 -2
- package/esm2022/lib/services/auth/types.mjs +1 -1
- package/esm2022/lib/services/image/image.service.mjs +244 -0
- package/esm2022/lib/services/image/index.mjs +3 -0
- package/esm2022/lib/services/image/types.mjs +13 -0
- package/esm2022/lib/version.mjs +2 -2
- package/esm2022/public-api.mjs +7 -1
- package/fesm2022/valtech-components.mjs +815 -7
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/molecules/image-crop/image-crop.component.d.ts +59 -0
- package/lib/components/molecules/image-crop/index.d.ts +1 -0
- package/lib/components/organisms/avatar-upload/avatar-upload.component.d.ts +82 -0
- package/lib/components/organisms/avatar-upload/types.d.ts +62 -0
- package/lib/components/templates/docs-page/docs-page.component.d.ts +3 -0
- package/lib/components/templates/docs-page/types.d.ts +39 -0
- package/lib/services/auth/auth.service.d.ts +6 -1
- package/lib/services/auth/types.d.ts +18 -0
- package/lib/services/image/image.service.d.ts +76 -0
- package/lib/services/image/index.d.ts +2 -0
- package/lib/services/image/types.d.ts +74 -0
- package/lib/version.d.ts +1 -1
- package/package.json +2 -1
- package/public-api.d.ts +4 -0
- package/src/lib/services/firebase/firebase-messaging-sw.js +134 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { EventEmitter } from '@angular/core';
|
|
2
|
+
import { ImageCroppedEvent } from 'ngx-image-cropper';
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
/**
|
|
5
|
+
* ImageCropComponent
|
|
6
|
+
*
|
|
7
|
+
* A modal-ready component for cropping images with a specified aspect ratio.
|
|
8
|
+
* Uses ngx-image-cropper internally and provides a simple interface.
|
|
9
|
+
*
|
|
10
|
+
* @example Inside an ion-modal
|
|
11
|
+
* ```html
|
|
12
|
+
* <ion-modal [isOpen]="showCropModal">
|
|
13
|
+
* <ng-template>
|
|
14
|
+
* <val-image-crop
|
|
15
|
+
* [image]="selectedFile"
|
|
16
|
+
* [aspectRatio]="1"
|
|
17
|
+
* [roundCropper]="true"
|
|
18
|
+
* (cropComplete)="onCropComplete($event)"
|
|
19
|
+
* (cancel)="showCropModal = false"
|
|
20
|
+
* />
|
|
21
|
+
* </ng-template>
|
|
22
|
+
* </ion-modal>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare class ImageCropComponent {
|
|
26
|
+
private i18n;
|
|
27
|
+
/** Image file to crop */
|
|
28
|
+
readonly image: import("@angular/core").InputSignal<File>;
|
|
29
|
+
/** Aspect ratio (1 for square, 16/9 for widescreen, etc.) */
|
|
30
|
+
readonly aspectRatio: import("@angular/core").InputSignal<number>;
|
|
31
|
+
/** Use round cropper (for avatars) */
|
|
32
|
+
readonly roundCropper: import("@angular/core").InputSignal<boolean>;
|
|
33
|
+
/** Resize output to specific width (0 = no resize) */
|
|
34
|
+
readonly resizeToWidth: import("@angular/core").InputSignal<number>;
|
|
35
|
+
/** i18n namespace for labels */
|
|
36
|
+
readonly i18nNamespace: import("@angular/core").InputSignal<string>;
|
|
37
|
+
/** Emitted when crop is confirmed with the cropped blob */
|
|
38
|
+
cropComplete: EventEmitter<Blob>;
|
|
39
|
+
/** Emitted when user cancels the crop */
|
|
40
|
+
cancel: EventEmitter<void>;
|
|
41
|
+
/** Emitted when image fails to load */
|
|
42
|
+
loadFailed: EventEmitter<void>;
|
|
43
|
+
/** Internal signal for cropped blob */
|
|
44
|
+
protected croppedBlob: import("@angular/core").WritableSignal<Blob>;
|
|
45
|
+
/** Computed text for cancel button */
|
|
46
|
+
protected cancelText: import("@angular/core").Signal<string>;
|
|
47
|
+
/** Computed text for confirm button */
|
|
48
|
+
protected confirmText: import("@angular/core").Signal<string>;
|
|
49
|
+
/** Computed text for title */
|
|
50
|
+
protected titleText: import("@angular/core").Signal<string>;
|
|
51
|
+
/** Handle crop event from ngx-image-cropper */
|
|
52
|
+
onImageCropped(event: ImageCroppedEvent): void;
|
|
53
|
+
/** Confirm and emit the cropped blob */
|
|
54
|
+
confirmCrop(): void;
|
|
55
|
+
/** Handle load failure */
|
|
56
|
+
onLoadFailed(): void;
|
|
57
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ImageCropComponent, never>;
|
|
58
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<ImageCropComponent, "val-image-crop", never, { "image": { "alias": "image"; "required": true; "isSignal": true; }; "aspectRatio": { "alias": "aspectRatio"; "required": false; "isSignal": true; }; "roundCropper": { "alias": "roundCropper"; "required": false; "isSignal": true; }; "resizeToWidth": { "alias": "resizeToWidth"; "required": false; "isSignal": true; }; "i18nNamespace": { "alias": "i18nNamespace"; "required": false; "isSignal": true; }; }, { "cropComplete": "cropComplete"; "cancel": "cancel"; "loadFailed": "loadFailed"; }, never, never, true, never>;
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './image-crop.component';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ElementRef, EventEmitter } from '@angular/core';
|
|
2
|
+
import { AvatarUploadError, AvatarUploadMetadata, AvatarUploadResult } from './types';
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
/**
|
|
5
|
+
* AvatarUploadComponent
|
|
6
|
+
*
|
|
7
|
+
* A complete avatar upload solution with:
|
|
8
|
+
* - Image selection from device
|
|
9
|
+
* - Crop modal with round preview
|
|
10
|
+
* - Automatic compression and thumbnail generation
|
|
11
|
+
* - Upload to Firebase Storage
|
|
12
|
+
* - Backend sync via AuthService
|
|
13
|
+
*
|
|
14
|
+
* @example Basic usage
|
|
15
|
+
* ```html
|
|
16
|
+
* <val-avatar-upload
|
|
17
|
+
* [props]="{
|
|
18
|
+
* currentUrl: user()?.avatarUrl,
|
|
19
|
+
* initials: 'JD',
|
|
20
|
+
* size: 120
|
|
21
|
+
* }"
|
|
22
|
+
* (uploaded)="onAvatarUploaded($event)"
|
|
23
|
+
* (error)="onError($event)"
|
|
24
|
+
* />
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare class AvatarUploadComponent {
|
|
28
|
+
private imageService;
|
|
29
|
+
private storageService;
|
|
30
|
+
private authService;
|
|
31
|
+
private i18n;
|
|
32
|
+
fileInput: ElementRef<HTMLInputElement>;
|
|
33
|
+
/** Component configuration */
|
|
34
|
+
readonly props: import("@angular/core").InputSignal<AvatarUploadMetadata>;
|
|
35
|
+
/** Emitted after successful upload and backend sync */
|
|
36
|
+
uploaded: EventEmitter<AvatarUploadResult>;
|
|
37
|
+
/** Emitted on any error during the process */
|
|
38
|
+
error: EventEmitter<AvatarUploadError>;
|
|
39
|
+
/** Emitted when upload starts */
|
|
40
|
+
uploadStart: EventEmitter<void>;
|
|
41
|
+
protected loading: import("@angular/core").WritableSignal<boolean>;
|
|
42
|
+
protected showCropModal: import("@angular/core").WritableSignal<boolean>;
|
|
43
|
+
protected selectedFile: import("@angular/core").WritableSignal<File>;
|
|
44
|
+
protected previewUrl: import("@angular/core").WritableSignal<string>;
|
|
45
|
+
protected imageLoadError: import("@angular/core").WritableSignal<boolean>;
|
|
46
|
+
/** Merged config with defaults */
|
|
47
|
+
protected config: import("@angular/core").Signal<{
|
|
48
|
+
currentUrl?: string;
|
|
49
|
+
initials?: string;
|
|
50
|
+
backgroundColor: string;
|
|
51
|
+
size: number;
|
|
52
|
+
editable: boolean;
|
|
53
|
+
storagePath: string;
|
|
54
|
+
i18nNamespace: string;
|
|
55
|
+
maxFileSize: number;
|
|
56
|
+
compressQuality: number;
|
|
57
|
+
maxWidth: number;
|
|
58
|
+
thumbnailSize: number;
|
|
59
|
+
}>;
|
|
60
|
+
/** URL to display (preview takes priority over current) */
|
|
61
|
+
protected displayUrl: import("@angular/core").Signal<string>;
|
|
62
|
+
/** Aria label for edit button */
|
|
63
|
+
protected editButtonLabel: import("@angular/core").Signal<string>;
|
|
64
|
+
/** Open file picker dialog */
|
|
65
|
+
openFilePicker(): void;
|
|
66
|
+
/** Handle file selection */
|
|
67
|
+
onFileSelected(event: Event): void;
|
|
68
|
+
/** Handle crop completion */
|
|
69
|
+
onCropComplete(croppedBlob: Blob): Promise<void>;
|
|
70
|
+
/** Handle crop cancel */
|
|
71
|
+
onCropCancel(): void;
|
|
72
|
+
/** Handle crop load failure */
|
|
73
|
+
onCropLoadFailed(): void;
|
|
74
|
+
/** Handle image load error */
|
|
75
|
+
onImageError(): void;
|
|
76
|
+
/** Process cropped image and upload */
|
|
77
|
+
private processAndUpload;
|
|
78
|
+
/** Emit error event */
|
|
79
|
+
private emitError;
|
|
80
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<AvatarUploadComponent, never>;
|
|
81
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<AvatarUploadComponent, "val-avatar-upload", never, { "props": { "alias": "props"; "required": false; "isSignal": true; }; }, { "uploaded": "uploaded"; "error": "error"; "uploadStart": "uploadStart"; }, never, never, true, never>;
|
|
82
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for AvatarUploadComponent
|
|
3
|
+
*/
|
|
4
|
+
export interface AvatarUploadMetadata {
|
|
5
|
+
/** Current avatar URL */
|
|
6
|
+
currentUrl?: string;
|
|
7
|
+
/** Initials to show when no avatar (e.g., "JD" for John Doe) */
|
|
8
|
+
initials?: string;
|
|
9
|
+
/** Background color for initials avatar */
|
|
10
|
+
backgroundColor?: string;
|
|
11
|
+
/** Avatar size in pixels (default: 100) */
|
|
12
|
+
size?: number;
|
|
13
|
+
/** Show edit button (default: true) */
|
|
14
|
+
editable?: boolean;
|
|
15
|
+
/** Storage path prefix without userId (default: 'avatars') */
|
|
16
|
+
storagePath?: string;
|
|
17
|
+
/** i18n namespace for labels (default: 'AvatarUpload') */
|
|
18
|
+
i18nNamespace?: string;
|
|
19
|
+
/** Max file size in bytes (default: 10MB) */
|
|
20
|
+
maxFileSize?: number;
|
|
21
|
+
/** Quality for compressed image 0-1 (default: 0.8) */
|
|
22
|
+
compressQuality?: number;
|
|
23
|
+
/** Max width for avatar (default: 800) */
|
|
24
|
+
maxWidth?: number;
|
|
25
|
+
/** Thumbnail size (default: 150) */
|
|
26
|
+
thumbnailSize?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Result emitted after successful upload
|
|
30
|
+
*/
|
|
31
|
+
export interface AvatarUploadResult {
|
|
32
|
+
/** Full-size avatar URL */
|
|
33
|
+
avatarUrl: string;
|
|
34
|
+
/** Thumbnail URL */
|
|
35
|
+
thumbnailUrl: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Error types that can occur during upload
|
|
39
|
+
*/
|
|
40
|
+
export type AvatarUploadErrorType = 'invalidType' | 'fileTooLarge' | 'uploadFailed' | 'backendFailed' | 'cancelled';
|
|
41
|
+
/**
|
|
42
|
+
* Error object emitted on failure
|
|
43
|
+
*/
|
|
44
|
+
export interface AvatarUploadError {
|
|
45
|
+
type: AvatarUploadErrorType;
|
|
46
|
+
message: string;
|
|
47
|
+
originalError?: unknown;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Default values
|
|
51
|
+
*/
|
|
52
|
+
export declare const AVATAR_UPLOAD_DEFAULTS: {
|
|
53
|
+
size: number;
|
|
54
|
+
editable: boolean;
|
|
55
|
+
storagePath: string;
|
|
56
|
+
i18nNamespace: string;
|
|
57
|
+
maxFileSize: number;
|
|
58
|
+
compressQuality: number;
|
|
59
|
+
maxWidth: number;
|
|
60
|
+
thumbnailSize: number;
|
|
61
|
+
backgroundColor: string;
|
|
62
|
+
};
|
|
@@ -2,6 +2,7 @@ import { AfterViewInit, OnDestroy } from '@angular/core';
|
|
|
2
2
|
import { DocsBreadcrumbMetadata } from '../../molecules/docs-breadcrumb/types';
|
|
3
3
|
import { DocsNavLinksMetadata } from '../../molecules/docs-nav-links/types';
|
|
4
4
|
import { DocsTocMetadata } from '../../organisms/docs-toc/types';
|
|
5
|
+
import { ContentReactionMetadata } from '../../molecules/content-reaction/types';
|
|
5
6
|
import { DocsPageMetadata } from './types';
|
|
6
7
|
import * as i0 from "@angular/core";
|
|
7
8
|
/**
|
|
@@ -41,6 +42,7 @@ import * as i0 from "@angular/core";
|
|
|
41
42
|
*/
|
|
42
43
|
export declare class DocsPageComponent implements AfterViewInit, OnDestroy {
|
|
43
44
|
private elementRef;
|
|
45
|
+
private router;
|
|
44
46
|
private _props;
|
|
45
47
|
set props(value: DocsPageMetadata);
|
|
46
48
|
get props(): DocsPageMetadata;
|
|
@@ -53,6 +55,7 @@ export declare class DocsPageComponent implements AfterViewInit, OnDestroy {
|
|
|
53
55
|
navLinksProps: import("@angular/core").Signal<DocsNavLinksMetadata>;
|
|
54
56
|
showNavLinks: import("@angular/core").Signal<boolean>;
|
|
55
57
|
breadcrumbProps: import("@angular/core").Signal<DocsBreadcrumbMetadata>;
|
|
58
|
+
feedbackProps: import("@angular/core").Signal<ContentReactionMetadata>;
|
|
56
59
|
static ɵfac: i0.ɵɵFactoryDeclaration<DocsPageComponent, never>;
|
|
57
60
|
static ɵcmp: i0.ɵɵComponentDeclaration<DocsPageComponent, "val-docs-page", never, { "props": { "alias": "props"; "required": false; }; }, {}, never, ["*"], true, never>;
|
|
58
61
|
}
|
|
@@ -66,6 +66,45 @@ export interface DocsPageMetadata {
|
|
|
66
66
|
* Custom CSS class.
|
|
67
67
|
*/
|
|
68
68
|
cssClass?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Feedback/reaction widget configuration.
|
|
71
|
+
* When enabled, shows a "Was this page helpful?" reaction widget.
|
|
72
|
+
*/
|
|
73
|
+
feedback?: DocsPageFeedbackConfig;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Configuration for the feedback reaction widget.
|
|
77
|
+
*/
|
|
78
|
+
export interface DocsPageFeedbackConfig {
|
|
79
|
+
/**
|
|
80
|
+
* Enable the feedback widget.
|
|
81
|
+
*/
|
|
82
|
+
enabled: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Entity type for categorizing feedback.
|
|
85
|
+
* @default 'docs-page'
|
|
86
|
+
*/
|
|
87
|
+
entityType?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Entity ID for this page. If not provided, derives from current route.
|
|
90
|
+
* @example 'components-button', 'guides-theming'
|
|
91
|
+
*/
|
|
92
|
+
entityId?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Custom question text. If not provided, uses i18n default.
|
|
95
|
+
* @example '¿Te resultó útil esta página?'
|
|
96
|
+
*/
|
|
97
|
+
question?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Allow anonymous (unauthenticated) feedback.
|
|
100
|
+
* @default true
|
|
101
|
+
*/
|
|
102
|
+
allowAnonymous?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Show optional comment field after reaction.
|
|
105
|
+
* @default true
|
|
106
|
+
*/
|
|
107
|
+
showComment?: boolean;
|
|
69
108
|
}
|
|
70
109
|
/**
|
|
71
110
|
* Link to another documentation page.
|
|
@@ -6,7 +6,7 @@ import { AuthStateService } from './auth-state.service';
|
|
|
6
6
|
import { TokenService } from './token.service';
|
|
7
7
|
import { AuthStorageService } from './storage.service';
|
|
8
8
|
import { AuthSyncService } from './sync.service';
|
|
9
|
-
import { SigninRequest, SigninResponse, SignupRequest, SignupResponse, VerifyEmailRequest, VerifyEmailResponse, ResendCodeRequest, ResendCodeResponse, MFAVerifyResponse, RefreshResponse, GetPermissionsResponse, GetProfileResponse, UpdateProfileRequest, UpdateProfileResponse, MFASetupResponse, MFAConfirmResponse, MFADisableResponse, ForgotPasswordRequest, ForgotPasswordResponse, ResetPasswordRequest, ResetPasswordResponse, ChangePasswordResponse, DeleteAccountResponse, SwitchOrgResponse, MFAMethod, AuthError, ValtechAuthConfig, EnableNotificationsResult, NotificationPermissionState, RegisterDeviceResult, TOTPSetupResponse, TOTPVerifySetupResponse, TOTPDisableResponse, RegenerateBackupCodesResponse, BackupCodesCountResponse, OAuthProvider, LinkedProvider, HasPasswordResponse, UpdateHandleResponse, CheckHandleResponse } from './types';
|
|
9
|
+
import { SigninRequest, SigninResponse, SignupRequest, SignupResponse, VerifyEmailRequest, VerifyEmailResponse, ResendCodeRequest, ResendCodeResponse, MFAVerifyResponse, RefreshResponse, GetPermissionsResponse, GetProfileResponse, UpdateProfileRequest, UpdateProfileResponse, MFASetupResponse, MFAConfirmResponse, MFADisableResponse, ForgotPasswordRequest, ForgotPasswordResponse, ResetPasswordRequest, ResetPasswordResponse, ChangePasswordResponse, DeleteAccountResponse, SwitchOrgResponse, MFAMethod, AuthError, ValtechAuthConfig, EnableNotificationsResult, NotificationPermissionState, RegisterDeviceResult, TOTPSetupResponse, TOTPVerifySetupResponse, TOTPDisableResponse, RegenerateBackupCodesResponse, BackupCodesCountResponse, OAuthProvider, LinkedProvider, HasPasswordResponse, UpdateHandleResponse, CheckHandleResponse, UpdateAvatarRequest, UpdateAvatarResponse } from './types';
|
|
10
10
|
import { OAuthService } from './oauth.service';
|
|
11
11
|
import { FirebaseService, MessagingService } from '../firebase';
|
|
12
12
|
import { I18nService } from '../i18n';
|
|
@@ -241,6 +241,11 @@ export declare class AuthService implements OnDestroy {
|
|
|
241
241
|
* Actualiza el perfil del usuario.
|
|
242
242
|
*/
|
|
243
243
|
updateProfile(request: UpdateProfileRequest): Observable<UpdateProfileResponse>;
|
|
244
|
+
/**
|
|
245
|
+
* Actualiza el avatar del usuario en el backend.
|
|
246
|
+
* Nota: El estado local del avatar se maneja a través de getProfile().
|
|
247
|
+
*/
|
|
248
|
+
updateAvatar(request: UpdateAvatarRequest): Observable<UpdateAvatarResponse>;
|
|
244
249
|
/**
|
|
245
250
|
* Inicia el proceso de recuperación de contraseña.
|
|
246
251
|
* Envía un código al email del usuario.
|
|
@@ -453,6 +453,24 @@ export interface UpdateProfileResponse {
|
|
|
453
453
|
operationId: string;
|
|
454
454
|
updated: boolean;
|
|
455
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Request para actualizar avatar del usuario.
|
|
458
|
+
*/
|
|
459
|
+
export interface UpdateAvatarRequest {
|
|
460
|
+
/** URL del avatar en Firebase Storage */
|
|
461
|
+
avatarUrl: string;
|
|
462
|
+
/** URL del thumbnail (opcional) */
|
|
463
|
+
avatarThumbnail?: string;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Response de actualizar avatar.
|
|
467
|
+
*/
|
|
468
|
+
export interface UpdateAvatarResponse {
|
|
469
|
+
operationId: string;
|
|
470
|
+
avatarUrl: string;
|
|
471
|
+
avatarThumbnail?: string;
|
|
472
|
+
updatedAt: string;
|
|
473
|
+
}
|
|
456
474
|
/**
|
|
457
475
|
* Request para cambiar de organización activa.
|
|
458
476
|
*/
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { CropData, ImageCompressOptions, ImageValidateOptions, ImageValidationResult, ProcessedImage } from './types';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
/**
|
|
4
|
+
* ImageService
|
|
5
|
+
*
|
|
6
|
+
* Service for image processing including compression, thumbnails, cropping and validation.
|
|
7
|
+
* Uses HTML Canvas for all operations - no external dependencies.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const imageService = inject(ImageService);
|
|
12
|
+
*
|
|
13
|
+
* // Compress an image
|
|
14
|
+
* const compressed = await imageService.compress(file, { maxWidth: 800, quality: 0.8 });
|
|
15
|
+
*
|
|
16
|
+
* // Generate thumbnail
|
|
17
|
+
* const thumb = await imageService.thumbnail(file, 150);
|
|
18
|
+
*
|
|
19
|
+
* // Validate before processing
|
|
20
|
+
* const validation = imageService.validate(file, { maxSize: 5 * 1024 * 1024 });
|
|
21
|
+
* if (!validation.valid) {
|
|
22
|
+
* console.error(validation.message);
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare class ImageService {
|
|
27
|
+
/**
|
|
28
|
+
* Compress an image maintaining aspect ratio
|
|
29
|
+
* @param file - File or Blob to compress
|
|
30
|
+
* @param options - Compression options
|
|
31
|
+
* @returns Promise with processed image data
|
|
32
|
+
*/
|
|
33
|
+
compress(file: File | Blob, options?: ImageCompressOptions): Promise<ProcessedImage>;
|
|
34
|
+
/**
|
|
35
|
+
* Generate a square thumbnail from an image
|
|
36
|
+
* @param file - File or Blob to process
|
|
37
|
+
* @param size - Thumbnail size in pixels (default: 150)
|
|
38
|
+
* @returns Promise with processed thumbnail
|
|
39
|
+
*/
|
|
40
|
+
thumbnail(file: File | Blob, size?: number): Promise<ProcessedImage>;
|
|
41
|
+
/**
|
|
42
|
+
* Crop an image with specific coordinates
|
|
43
|
+
* @param file - File or Blob to crop
|
|
44
|
+
* @param cropData - Crop coordinates and dimensions
|
|
45
|
+
* @param options - Optional compression options for output
|
|
46
|
+
* @returns Promise with cropped image
|
|
47
|
+
*/
|
|
48
|
+
crop(file: File | Blob, cropData: CropData, options?: ImageCompressOptions): Promise<ProcessedImage>;
|
|
49
|
+
/**
|
|
50
|
+
* Validate an image file before processing
|
|
51
|
+
* @param file - File to validate
|
|
52
|
+
* @param options - Validation options
|
|
53
|
+
* @returns Validation result with error details if invalid
|
|
54
|
+
*/
|
|
55
|
+
validate(file: File, options?: ImageValidateOptions): ImageValidationResult;
|
|
56
|
+
/**
|
|
57
|
+
* Validate image dimensions (async - requires loading image)
|
|
58
|
+
* @param file - File to validate
|
|
59
|
+
* @param options - Validation options with minWidth/minHeight
|
|
60
|
+
* @returns Promise with validation result
|
|
61
|
+
*/
|
|
62
|
+
validateDimensions(file: File, options: Pick<ImageValidateOptions, 'minWidth' | 'minHeight'>): Promise<ImageValidationResult>;
|
|
63
|
+
/**
|
|
64
|
+
* Convert a Blob/File to a data URL
|
|
65
|
+
*/
|
|
66
|
+
toDataUrl(file: File | Blob): Promise<string>;
|
|
67
|
+
/**
|
|
68
|
+
* Convert a data URL to a Blob
|
|
69
|
+
*/
|
|
70
|
+
dataUrlToBlob(dataUrl: string): Blob;
|
|
71
|
+
private loadImage;
|
|
72
|
+
private calculateDimensions;
|
|
73
|
+
private canvasToBlob;
|
|
74
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ImageService, never>;
|
|
75
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<ImageService>;
|
|
76
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for image compression
|
|
3
|
+
*/
|
|
4
|
+
export interface ImageCompressOptions {
|
|
5
|
+
/** Maximum width in pixels (default: 800) */
|
|
6
|
+
maxWidth?: number;
|
|
7
|
+
/** Maximum height in pixels (default: 800) */
|
|
8
|
+
maxHeight?: number;
|
|
9
|
+
/** Quality 0-1 (default: 0.8) */
|
|
10
|
+
quality?: number;
|
|
11
|
+
/** Output MIME type (default: 'image/jpeg') */
|
|
12
|
+
mimeType?: 'image/jpeg' | 'image/png' | 'image/webp';
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Options for image validation
|
|
16
|
+
*/
|
|
17
|
+
export interface ImageValidateOptions {
|
|
18
|
+
/** Maximum file size in bytes (default: 10MB) */
|
|
19
|
+
maxSize?: number;
|
|
20
|
+
/** Allowed MIME types (default: ['image/jpeg', 'image/png', 'image/webp', 'image/gif']) */
|
|
21
|
+
allowedTypes?: string[];
|
|
22
|
+
/** Minimum width in pixels */
|
|
23
|
+
minWidth?: number;
|
|
24
|
+
/** Minimum height in pixels */
|
|
25
|
+
minHeight?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Result of image validation
|
|
29
|
+
*/
|
|
30
|
+
export interface ImageValidationResult {
|
|
31
|
+
valid: boolean;
|
|
32
|
+
error?: 'invalidType' | 'fileTooLarge' | 'imageTooSmall';
|
|
33
|
+
message?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Processed image result
|
|
37
|
+
*/
|
|
38
|
+
export interface ProcessedImage {
|
|
39
|
+
/** Processed image as Blob */
|
|
40
|
+
blob: Blob;
|
|
41
|
+
/** Data URL for preview */
|
|
42
|
+
dataUrl: string;
|
|
43
|
+
/** Final width in pixels */
|
|
44
|
+
width: number;
|
|
45
|
+
/** Final height in pixels */
|
|
46
|
+
height: number;
|
|
47
|
+
/** File size in bytes */
|
|
48
|
+
size: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Crop data for manual cropping
|
|
52
|
+
*/
|
|
53
|
+
export interface CropData {
|
|
54
|
+
/** X position of crop area */
|
|
55
|
+
x: number;
|
|
56
|
+
/** Y position of crop area */
|
|
57
|
+
y: number;
|
|
58
|
+
/** Width of crop area */
|
|
59
|
+
width: number;
|
|
60
|
+
/** Height of crop area */
|
|
61
|
+
height: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Default values for image processing
|
|
65
|
+
*/
|
|
66
|
+
export declare const IMAGE_DEFAULTS: {
|
|
67
|
+
maxWidth: number;
|
|
68
|
+
maxHeight: number;
|
|
69
|
+
quality: number;
|
|
70
|
+
mimeType: "image/jpeg";
|
|
71
|
+
maxSize: number;
|
|
72
|
+
allowedTypes: string[];
|
|
73
|
+
thumbnailSize: number;
|
|
74
|
+
};
|
package/lib/version.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "valtech-components",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.682",
|
|
4
4
|
"private": false,
|
|
5
5
|
"bin": {
|
|
6
6
|
"valtech-firebase-config": "./src/lib/services/firebase/scripts/generate-sw-config.js"
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@capacitor/browser": "^6.0.3",
|
|
34
34
|
"ng-otp-input": "^1.9.3",
|
|
35
|
+
"ngx-image-cropper": "^9.0.0",
|
|
35
36
|
"tslib": "^2.3.0"
|
|
36
37
|
},
|
|
37
38
|
"sideEffects": false,
|
package/public-api.d.ts
CHANGED
|
@@ -169,6 +169,7 @@ export * from './lib/components/molecules/username-input/username-input.componen
|
|
|
169
169
|
export * from './lib/components/molecules/username-input/types';
|
|
170
170
|
export * from './lib/components/molecules/linked-providers/linked-providers.component';
|
|
171
171
|
export * from './lib/components/molecules/linked-providers/types';
|
|
172
|
+
export * from './lib/components/molecules/image-crop/image-crop.component';
|
|
172
173
|
export * from './lib/components/organisms/article/article.component';
|
|
173
174
|
export * from './lib/components/organisms/article/types';
|
|
174
175
|
export * from './lib/components/organisms/banner/banner.component';
|
|
@@ -214,6 +215,8 @@ export * from './lib/components/organisms/terminal-404/terminal-404.component';
|
|
|
214
215
|
export * from './lib/components/organisms/terminal-404/types';
|
|
215
216
|
export * from './lib/components/organisms/bottom-nav/bottom-nav.component';
|
|
216
217
|
export * from './lib/components/organisms/bottom-nav/types';
|
|
218
|
+
export * from './lib/components/organisms/avatar-upload/avatar-upload.component';
|
|
219
|
+
export * from './lib/components/organisms/avatar-upload/types';
|
|
217
220
|
export * from './lib/components/templates/layout/layout.component';
|
|
218
221
|
export * from './lib/components/templates/simple/simple.component';
|
|
219
222
|
export * from './lib/components/templates/simple/types';
|
|
@@ -251,6 +254,7 @@ export * from './lib/services/app-config';
|
|
|
251
254
|
export * from './lib/services/presets';
|
|
252
255
|
export * from './lib/services/skeleton';
|
|
253
256
|
export * from './lib/services/pagination';
|
|
257
|
+
export * from './lib/services/image';
|
|
254
258
|
export * from './lib/services/ads';
|
|
255
259
|
export * from './lib/components/molecules/ad-slot/ad-slot.component';
|
|
256
260
|
export * from './lib/services/feedback';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Messaging Service Worker
|
|
3
|
+
*
|
|
4
|
+
* Service Worker estático para Firebase Cloud Messaging.
|
|
5
|
+
* Carga la configuración dinámicamente desde /firebase-config.js.
|
|
6
|
+
*
|
|
7
|
+
* CONFIGURACIÓN:
|
|
8
|
+
* 1. Crea firebase.config.json con tu configuración de Firebase
|
|
9
|
+
* 2. Ejecuta: npm run generate:firebase-config
|
|
10
|
+
* Esto genera /firebase-config.js con: self.FIREBASE_CONFIG = {...}
|
|
11
|
+
* 3. Agrega este SW y firebase-config.js a los assets de angular.json
|
|
12
|
+
*
|
|
13
|
+
* Ver README.md para documentación completa.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Importar Firebase scripts
|
|
17
|
+
importScripts('https://www.gstatic.com/firebasejs/10.7.0/firebase-app-compat.js');
|
|
18
|
+
importScripts('https://www.gstatic.com/firebasejs/10.7.0/firebase-messaging-compat.js');
|
|
19
|
+
|
|
20
|
+
// Importar configuración desde archivo externo (generado en build)
|
|
21
|
+
// Este archivo define: self.FIREBASE_CONFIG = { ... }
|
|
22
|
+
try {
|
|
23
|
+
importScripts('/firebase-config.js');
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error('[SW] No se pudo cargar firebase-config.js:', e);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Verificar que la configuración existe
|
|
29
|
+
if (!self.FIREBASE_CONFIG) {
|
|
30
|
+
console.error('[SW] FIREBASE_CONFIG no está definido.');
|
|
31
|
+
console.error('[SW] Ejecuta: npm run generate:firebase-config');
|
|
32
|
+
} else {
|
|
33
|
+
// Inicializar Firebase
|
|
34
|
+
firebase.initializeApp(self.FIREBASE_CONFIG);
|
|
35
|
+
|
|
36
|
+
// Obtener instancia de messaging
|
|
37
|
+
const messaging = firebase.messaging();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Handler para mensajes en background.
|
|
41
|
+
*/
|
|
42
|
+
messaging.onBackgroundMessage((payload) => {
|
|
43
|
+
console.log('[SW] Mensaje recibido en background:', payload);
|
|
44
|
+
|
|
45
|
+
const notificationTitle = payload.notification?.title || 'Nueva notificación';
|
|
46
|
+
const notificationOptions = {
|
|
47
|
+
body: payload.notification?.body || '',
|
|
48
|
+
icon: payload.notification?.icon || '/assets/icon/favicon.ico',
|
|
49
|
+
image: payload.notification?.image,
|
|
50
|
+
badge: '/assets/icon/badge.png',
|
|
51
|
+
tag: payload.messageId || 'default',
|
|
52
|
+
data: {
|
|
53
|
+
...payload.data,
|
|
54
|
+
messageId: payload.messageId,
|
|
55
|
+
title: notificationTitle,
|
|
56
|
+
body: payload.notification?.body,
|
|
57
|
+
},
|
|
58
|
+
vibrate: [200, 100, 200],
|
|
59
|
+
requireInteraction: payload.data?.require_interaction === 'true',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return self.registration.showNotification(notificationTitle, notificationOptions);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Handler para clicks en notificaciones.
|
|
67
|
+
*/
|
|
68
|
+
self.addEventListener('notificationclick', (event) => {
|
|
69
|
+
console.log('[SW] Click en notificación:', event);
|
|
70
|
+
event.notification.close();
|
|
71
|
+
|
|
72
|
+
const data = event.notification.data || {};
|
|
73
|
+
let targetUrl = '/';
|
|
74
|
+
|
|
75
|
+
if (data.route) {
|
|
76
|
+
targetUrl = data.route;
|
|
77
|
+
} else if (data.url) {
|
|
78
|
+
targetUrl = data.url;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (data.query_params) {
|
|
82
|
+
const separator = targetUrl.includes('?') ? '&' : '?';
|
|
83
|
+
targetUrl += separator + data.query_params;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const notificationPayload = {
|
|
87
|
+
type: 'NOTIFICATION_CLICK',
|
|
88
|
+
notification: {
|
|
89
|
+
title: data.title,
|
|
90
|
+
body: data.body,
|
|
91
|
+
data: data,
|
|
92
|
+
messageId: data.messageId,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
event.waitUntil(
|
|
97
|
+
clients
|
|
98
|
+
.matchAll({ type: 'window', includeUncontrolled: true })
|
|
99
|
+
.then((clientList) => {
|
|
100
|
+
// Siempre enviar postMessage para que la app pueda reaccionar
|
|
101
|
+
for (const client of clientList) {
|
|
102
|
+
client.postMessage(notificationPayload);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Navegar si hay route o url
|
|
106
|
+
if (targetUrl !== '/') {
|
|
107
|
+
for (const client of clientList) {
|
|
108
|
+
if ('navigate' in client) {
|
|
109
|
+
return client.navigate(targetUrl).then((c) => c?.focus());
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Si no hay cliente abierto, abrir nueva ventana
|
|
113
|
+
if (clients.openWindow) {
|
|
114
|
+
return clients.openWindow(targetUrl);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Solo hacer focus si no hay navegación
|
|
119
|
+
for (const client of clientList) {
|
|
120
|
+
if ('focus' in client) {
|
|
121
|
+
return client.focus();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (clients.openWindow) {
|
|
125
|
+
return clients.openWindow('/');
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
self.addEventListener('notificationclose', (event) => {
|
|
132
|
+
console.log('[SW] Notificación cerrada:', event.notification.tag);
|
|
133
|
+
});
|
|
134
|
+
}
|