Skip to content

Commit 4757725

Browse files
committed
Add UI for image upload
- Disable image upload until we have a service for it.
1 parent 849faa0 commit 4757725

File tree

3 files changed

+336
-17
lines changed

3 files changed

+336
-17
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
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(/^https?:\/\/.+/)) {
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+
}

src/app/pages/profile/profile.component.ts

+17-17
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import NDK, {
1313
NDKUser,
1414
} from '@nostr-dev-kit/ndk';
1515
import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
16+
import { ImageUploadComponent } from '../../components/image-upload.component';
1617

1718
export interface NostrProfile {
1819
name: string;
@@ -54,7 +55,8 @@ interface MediaItem {
5455
BreadcrumbComponent,
5556
FormsModule,
5657
SigningDialogComponent,
57-
DragDropModule
58+
DragDropModule,
59+
ImageUploadComponent
5860
],
5961
template: `
6062
<section class="hero">
@@ -120,24 +122,22 @@ interface MediaItem {
120122
></textarea>
121123
</div>
122124
123-
<div class="form-group">
124-
<label for="picture">Profile Picture URL</label>
125-
<input
126-
id="picture"
127-
type="url"
128-
[(ngModel)]="profile.picture"
129-
placeholder="https://example.com/picture.jpg"
130-
/>
125+
<div class="form-group full-width">
126+
<label>Profile Picture</label>
127+
<app-image-upload
128+
label="Profile Picture"
129+
[imageUrl]="profile.picture"
130+
(urlChanged)="profile.picture = $event"
131+
></app-image-upload>
131132
</div>
132133
133-
<div class="form-group">
134-
<label for="banner">Banner Image URL</label>
135-
<input
136-
id="banner"
137-
type="url"
138-
[(ngModel)]="profile.banner"
139-
placeholder="https://example.com/banner.jpg"
140-
/>
134+
<div class="form-group full-width">
135+
<label>Banner Image</label>
136+
<app-image-upload
137+
label="Banner Image"
138+
[imageUrl]="profile.banner"
139+
(urlChanged)="profile.banner = $event"
140+
></app-image-upload>
141141
</div>
142142
143143
<div class="form-group">

src/environment.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ import { version } from '../package.json';
22

33
export const environment = {
44
appVersion: version,
5+
imgbbApiKey: 'YOUR_IMGBB_API_KEY', // Replace this with your actual ImgBB API key
56
};

0 commit comments

Comments
 (0)