feat: add reusable FileUploaderComponent with media previews

This commit is contained in:
Azmat-7860
2026-11-04 23:13:04 +05:30
parent 39f602f3f6
commit 7605927b90
5 changed files with 285 additions and 3 deletions
@@ -0,0 +1,57 @@
<div class="file-uploader-container">
<label class="clr-control-label" *ngIf="label">{{ label }}</label>
<!-- Upload progress spinner -->
<div *ngIf="uploading" class="uploading-state">
<clr-spinner clrInline clrSmall></clr-spinner>
<span class="upload-text">Uploading...</span>
</div>
<!-- When no file is uploaded yet -->
<div *ngIf="!value && !uploading" class="upload-input-row">
<label class="btn btn-sm btn-outline-primary upload-btn">
<clr-icon shape="upload"></clr-icon> Choose {{ type | titlecase }}
<input
type="file"
[accept]="acceptTypes"
[disabled]="disabled"
(change)="onFileSelected($event)"
class="hidden-file-input"
/>
</label>
</div>
<!-- Preview when file is uploaded -->
<div *ngIf="value && !uploading" class="preview-container">
<!-- Image preview -->
<div *ngIf="type === 'image'" class="image-preview">
<img [src]="fileUrl" alt="{{ value }}" (error)="onImgError($event)" />
</div>
<!-- Audio preview -->
<div *ngIf="type === 'audio'" class="audio-preview">
<audio controls [src]="fileUrl"></audio>
</div>
<!-- Video preview -->
<div *ngIf="type === 'video'" class="video-preview">
<video controls [src]="fileUrl"></video>
</div>
<!-- File / document link -->
<div class="file-meta">
<clr-icon shape="file" class="file-icon"></clr-icon>
<a [href]="fileUrl" target="_blank" class="file-name" [title]="value">{{ value }}</a>
<button
type="button"
class="btn btn-sm btn-link btn-danger remove-btn"
(click)="removeFile()"
[disabled]="disabled"
title="Remove file"
>
<clr-icon shape="trash"></clr-icon>
</button>
</div>
</div>
</div>
@@ -0,0 +1,102 @@
.file-uploader-container {
margin-bottom: 0.5rem;
.clr-control-label {
display: block;
font-size: 0.65rem;
font-weight: 600;
margin-bottom: 0.25rem;
color: #444;
}
.upload-input-row {
margin-top: 0.25rem;
.upload-btn {
position: relative;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.35rem;
margin: 0;
.hidden-file-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
}
}
.uploading-state {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0;
color: #555;
font-size: 0.75rem;
}
.preview-container {
margin-top: 0.35rem;
padding: 0.4rem;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #fafafa;
.image-preview img {
max-width: 100%;
max-height: 120px;
object-fit: cover;
border-radius: 3px;
margin-bottom: 0.3rem;
display: block;
}
.audio-preview audio {
width: 100%;
height: 36px;
margin-bottom: 0.3rem;
}
.video-preview video {
max-width: 100%;
max-height: 140px;
border-radius: 3px;
margin-bottom: 0.3rem;
display: block;
}
.file-meta {
display: flex;
align-items: center;
gap: 0.35rem;
font-size: 0.75rem;
.file-icon {
flex-shrink: 0;
}
.file-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-grow: 1;
color: #0072a3;
text-decoration: underline;
}
.remove-btn {
margin: 0;
padding: 0 0.25rem;
min-width: auto;
color: #c92c2c;
}
}
}
}
@@ -0,0 +1,122 @@
import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr';
import { AppConfig } from 'src/app/app-config';
@Component({
selector: 'app-file-uploader',
templateUrl: './file-uploader.component.html',
styleUrls: ['./file-uploader.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FileUploaderComponent),
multi: true
}
]
})
export class FileUploaderComponent implements ControlValueAccessor {
@Input() label: string = 'Upload File';
@Input() type: 'file' | 'image' | 'audio' | 'video' = 'file';
@Input() ref: string = 'common';
@Input() tableName: string = 'files';
value: string = '';
uploading: boolean = false;
disabled: boolean = false;
private onChange: (val: any) => void = () => {};
private onTouched: () => void = () => {};
constructor(
private http: HttpClient,
private appConfig: AppConfig,
private toastr: ToastrService
) {}
get acceptTypes(): string {
switch (this.type) {
case 'image':
return 'image/*';
case 'audio':
return 'audio/*';
case 'video':
return 'video/*';
default:
return '*/*';
}
}
get fileUrl(): string {
if (!this.value) {
return '';
}
if (this.value.startsWith('http://') || this.value.startsWith('https://')) {
return this.value;
}
const base = this.appConfig?.baseApiPath || '';
const cleanBase = base.endsWith('/') ? base : base + '/';
return `${cleanBase}FileUpload/files/${this.value}`;
}
writeValue(val: any): void {
this.value = val || '';
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onFileSelected(event: any): void {
const file = event.target.files && event.target.files[0];
if (!file) {
return;
}
this.uploading = true;
const formData = new FormData();
formData.append('file', file);
const base = this.appConfig?.baseApiPath || '';
const cleanBase = base.endsWith('/') ? base : base + '/';
const uploadUrl = `${cleanBase}FileUpload/Uploadeddocs/${this.ref}/${this.tableName}`;
this.http.post<any>(uploadUrl, formData).subscribe({
next: (res) => {
this.uploading = false;
const filename = res?.uploadedfile_name || file.name;
this.value = filename;
this.onChange(this.value);
this.onTouched();
this.toastr.success('File uploaded successfully');
event.target.value = '';
},
error: (err) => {
this.uploading = false;
console.error('File upload failed', err);
this.toastr.error('File upload failed');
event.target.value = '';
}
});
}
removeFile(): void {
this.value = '';
this.onChange('');
this.onTouched();
}
onImgError(event: any): void {
console.warn('Image failed to load:', this.fileUrl);
}
}
@@ -106,6 +106,7 @@ import { MappingruleaddComponent } from './datamanagement/mappingrule/mappingrul
import { MappingruleallComponent } from './datamanagement/mappingrule/mappingruleall/mappingruleall.component';
import { MappingruleeditComponent } from './datamanagement/mappingrule/mappingruleedit/mappingruleedit.component';
import { Stepper_workflowComponent } from './BuilderComponents/stepperworkflow/Stepper_workflow/Stepper_workflow.component';
import { FileUploaderComponent } from './BuilderComponents/file-uploader/file-uploader.component';
import { Token_registeryComponent } from './fnd/Token_registery/Token_registery.component';
import { MyworkspaceComponent } from './admin/myworkspace/myworkspace.component';
import { DynamicformComponent } from './superadmin/dynamicform/dynamicform.component';
@@ -140,6 +141,7 @@ import { DynamicFormViewerComponent } from './superadmin/dynamicform/dynamic-for
// buildercomponents
FileUploaderComponent,
@@ -10,8 +10,7 @@ export const environment = {
// changeble url // port
captchaSiteKey: '6LfrdSUpAAAAALkYDmnvdX3GLLCArgPWNHfXasjP',
//Deployment configs
//Deployment configs
};