1
+ import { Component , Input , Output , EventEmitter } from '@angular/core' ;
2
+ import { CommonModule } from '@angular/common' ;
3
+ import { FormsModule } from '@angular/forms' ;
4
+ import { HttpClient } from '@angular/common/http' ;
5
+ import { environment } from '../../environment' ;
6
+ import { ImagePopupComponent } from './image-popup.component' ;
7
+
8
+ @Component ( {
9
+ selector : 'app-image-upload' ,
10
+ standalone : true ,
11
+ imports : [ CommonModule , FormsModule , ImagePopupComponent ] ,
12
+ template : `
13
+ <div class="image-upload">
14
+ <div
15
+ class="preview-container"
16
+ (click)="imageUrl ? showPreview() : triggerFileInput()"
17
+ [class.has-image]="imageUrl"
18
+ >
19
+ <img *ngIf="imageUrl" [src]="imageUrl" [alt]="label" class="preview-image">
20
+ <div *ngIf="!imageUrl" class="upload-placeholder">
21
+ <!-- <i class="fas fa-image"></i> -->
22
+ <!-- <span>Click to {{ imageUrl ? 'view' : 'upload' }} {{label.toLowerCase()}}</span> -->
23
+ </div>
24
+
25
+ <div *ngIf="imageUrl" class="image-actions">
26
+ <!-- Enable when we have upload service. -->
27
+ <!-- <button class="action-button" (click)="triggerFileInput(); $event.stopPropagation()">
28
+ <i class="fas fa-upload"></i>
29
+ </button> -->
30
+ <button class="action-button" (click)="clearImage($event)">
31
+ <i class="fas fa-times"></i>
32
+ </button>
33
+ </div>
34
+ </div>
35
+
36
+ <div class="url-input">
37
+ <div class="input-wrapper">
38
+ <input
39
+ type="url"
40
+ [placeholder]="'Enter ' + label.toLowerCase() + ' URL'"
41
+ [(ngModel)]="imageUrl"
42
+ (ngModelChange)="onUrlChange($event)"
43
+ >
44
+ <button *ngIf="imageUrl" class="clear-button" (click)="clearImage($event)">
45
+ <i class="fas fa-times"></i>
46
+ </button>
47
+ </div>
48
+ <small class="helper-text">Drop an image file, paste a URL, or click to upload</small>
49
+ </div>
50
+
51
+ <input
52
+ #fileInput
53
+ type="file"
54
+ accept="image/*"
55
+ (change)="onFileSelected($event)"
56
+ style="display: none"
57
+ >
58
+
59
+ <div *ngIf="uploading" class="upload-overlay">
60
+ <div class="spinner"></div>
61
+ <span>Uploading...</span>
62
+ </div>
63
+ </div>
64
+
65
+ <app-image-popup
66
+ *ngIf="showPopup"
67
+ [imageUrl]="imageUrl"
68
+ [altText]="label"
69
+ (close)="showPopup = false"
70
+ ></app-image-popup>
71
+ ` ,
72
+ styles : [ `
73
+ .image-upload {
74
+ position: relative;
75
+ width: 100%;
76
+ }
77
+
78
+ .preview-container {
79
+ width: 100%;
80
+ aspect-ratio: 16/9;
81
+ background: var(--surface-ground);
82
+ border: 2px dashed var(--border);
83
+ border-radius: 8px;
84
+ cursor: pointer;
85
+ overflow: hidden;
86
+ transition: all 0.2s ease;
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ margin-bottom: 0.5rem;
91
+ position: relative;
92
+ }
93
+
94
+ .preview-container:hover {
95
+ border-color: var(--accent);
96
+ background: var(--surface-hover);
97
+ }
98
+
99
+ .preview-container.has-image {
100
+ border-style: solid;
101
+ }
102
+
103
+ .preview-container.has-image:hover .image-actions {
104
+ opacity: 1;
105
+ }
106
+
107
+ .preview-image {
108
+ width: 100%;
109
+ height: 100%;
110
+ object-fit: cover;
111
+ transition: filter 0.2s ease;
112
+ }
113
+
114
+ .preview-container:hover .preview-image {
115
+ filter: brightness(0.7);
116
+ }
117
+
118
+ .image-actions {
119
+ position: absolute;
120
+ top: 50%;
121
+ left: 50%;
122
+ transform: translate(-50%, -50%);
123
+ display: flex;
124
+ gap: 1rem;
125
+ opacity: 0;
126
+ transition: opacity 0.2s ease;
127
+ }
128
+
129
+ .action-button {
130
+ background: rgba(0, 0, 0, 0.6);
131
+ border: none;
132
+ color: white;
133
+ width: 40px;
134
+ height: 40px;
135
+ border-radius: 50%;
136
+ cursor: pointer;
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: center;
140
+ transition: all 0.2s ease;
141
+ }
142
+
143
+ .action-button:hover {
144
+ background: var(--accent);
145
+ transform: scale(1.1);
146
+ }
147
+
148
+ .upload-placeholder {
149
+ display: flex;
150
+ flex-direction: column;
151
+ align-items: center;
152
+ gap: 0.5rem;
153
+ color: var(--text-secondary);
154
+ padding: 2rem;
155
+ text-align: center;
156
+ }
157
+
158
+ .upload-placeholder i {
159
+ font-size: 2rem;
160
+ }
161
+
162
+ .url-input {
163
+ width: 100%;
164
+ }
165
+
166
+ .input-wrapper {
167
+ position: relative;
168
+ display: flex;
169
+ align-items: center;
170
+ }
171
+
172
+ .url-input input {
173
+ width: 100%;
174
+ padding: 0.75rem;
175
+ padding-right: 2.5rem;
176
+ border: 1px solid var(--border);
177
+ border-radius: 4px;
178
+ background: var(--surface-ground);
179
+ color: var(--text);
180
+ font-size: 1rem;
181
+ }
182
+
183
+ .url-input input:focus {
184
+ outline: none;
185
+ border-color: var(--accent);
186
+ }
187
+
188
+ .clear-button {
189
+ position: absolute;
190
+ right: 0.5rem;
191
+ background: none;
192
+ border: none;
193
+ color: var(--text-secondary);
194
+ cursor: pointer;
195
+ padding: 0.5rem;
196
+ }
197
+
198
+ .clear-button:hover {
199
+ color: var(--accent);
200
+ }
201
+
202
+ .helper-text {
203
+ display: block;
204
+ color: var(--text-secondary);
205
+ font-size: 0.85rem;
206
+ margin-top: 0.25rem;
207
+ }
208
+
209
+ .upload-overlay {
210
+ position: absolute;
211
+ top: 0;
212
+ left: 0;
213
+ right: 0;
214
+ bottom: 0;
215
+ background: rgba(0, 0, 0, 0.7);
216
+ display: flex;
217
+ flex-direction: column;
218
+ align-items: center;
219
+ justify-content: center;
220
+ gap: 1rem;
221
+ color: white;
222
+ border-radius: 8px;
223
+ backdrop-filter: blur(4px);
224
+ }
225
+
226
+ .spinner {
227
+ width: 40px;
228
+ height: 40px;
229
+ border: 4px solid rgba(255, 255, 255, 0.3);
230
+ border-top: 4px solid white;
231
+ border-radius: 50%;
232
+ animation: spin 1s linear infinite;
233
+ }
234
+
235
+ @keyframes spin {
236
+ 0% { transform: rotate(0deg); }
237
+ 100% { transform: rotate(360deg); }
238
+ }
239
+ ` ]
240
+ } )
241
+ export class ImageUploadComponent {
242
+ @Input ( ) label : string = 'Image' ;
243
+ @Input ( ) imageUrl : string = '' ;
244
+ @Output ( ) urlChanged = new EventEmitter < string > ( ) ;
245
+
246
+ uploading = false ;
247
+ showPopup = false ;
248
+ private readonly maxFileSize = 5 * 1024 * 1024 ; // 5MB
249
+
250
+ constructor ( private http : HttpClient ) { }
251
+
252
+ triggerFileInput ( ) {
253
+ // const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
254
+ // fileInput?.click();
255
+ }
256
+
257
+ showPreview ( ) {
258
+ if ( this . imageUrl ) {
259
+ this . showPopup = true ;
260
+ }
261
+ }
262
+
263
+ clearImage ( event : Event ) {
264
+ event . stopPropagation ( ) ;
265
+ this . imageUrl = '' ;
266
+ this . urlChanged . emit ( '' ) ;
267
+ }
268
+
269
+ onUrlChange ( url : string ) {
270
+ // Basic URL validation
271
+ if ( url && ! url . match ( / ^ h t t p s ? : \/ \/ .+ / ) ) {
272
+ url = 'https://' + url ;
273
+ }
274
+ this . urlChanged . emit ( url ) ;
275
+ }
276
+
277
+ async onFileSelected ( event : Event ) {
278
+ const file = ( event . target as HTMLInputElement ) . files ?. [ 0 ] ;
279
+ if ( ! file ) return ;
280
+
281
+ // Validate file size
282
+ if ( file . size > this . maxFileSize ) {
283
+ alert ( 'File size should be less than 5MB' ) ;
284
+ return ;
285
+ }
286
+
287
+ // Validate file type
288
+ if ( ! file . type . startsWith ( 'image/' ) ) {
289
+ alert ( 'Only image files are allowed' ) ;
290
+ return ;
291
+ }
292
+
293
+ this . uploading = true ;
294
+ try {
295
+ const formData = new FormData ( ) ;
296
+ formData . append ( 'image' , file ) ;
297
+
298
+ // Upload to ImgBB
299
+ const response = await fetch ( 'https://api.imgbb.com/1/upload?key=' + environment . imgbbApiKey , {
300
+ method : 'POST' ,
301
+ body : formData
302
+ } ) ;
303
+
304
+ const data = await response . json ( ) ;
305
+ if ( data . success ) {
306
+ this . imageUrl = data . data . url ;
307
+ this . urlChanged . emit ( this . imageUrl ) ;
308
+ } else {
309
+ throw new Error ( 'Upload failed' ) ;
310
+ }
311
+ } catch ( error ) {
312
+ console . error ( 'Error uploading image:' , error ) ;
313
+ alert ( 'Failed to upload image. Please try again or use an image URL instead.' ) ;
314
+ } finally {
315
+ this . uploading = false ;
316
+ }
317
+ }
318
+ }
0 commit comments