feat: add reusable FileUploaderComponent with media previews
This commit is contained in:
+57
@@ -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>
|
||||
|
||||
+102
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+122
@@ -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
|
||||
|
||||
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user