feat(project-editor): reusable image-field (thumbnail+replace+remove), branding OG image+gallery
- shared app-image-field component: thumbnail preview, replace, remove, opens media-picker - branding: add socialImageUrl + galleryUrls fields, wire to new image-field - footer: logo + payment icon fields use image-field (drop manual media-picker plumbing) - i18n: common.remove, adminCategories.replaceImage, builder.socialImage/gallery keys (en/ru/hy) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@ export interface BrandingConfig {
|
||||
logoCompactUrl?: UrlString;
|
||||
faviconUrl: UrlString;
|
||||
appIconUrl?: UrlString;
|
||||
socialImageUrl?: UrlString;
|
||||
galleryUrls?: UrlString[];
|
||||
supportEmail?: string;
|
||||
supportPhone?: string;
|
||||
}
|
||||
|
||||
19
src/app/shared/ui/image-field/image-field.component.html
Normal file
19
src/app/shared/ui/image-field/image-field.component.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<div class="image-field">
|
||||
@if (value()) {
|
||||
<img class="image-field__thumb" [src]="value()" alt="" />
|
||||
} @else {
|
||||
<div class="image-field__thumb image-field__thumb--empty"></div>
|
||||
}
|
||||
<div class="image-field__controls">
|
||||
<app-input [ngModel]="value() || ''" [disabled]="disabled()" (ngModelChange)="setValue($event)" />
|
||||
<div class="image-field__actions">
|
||||
<app-button variant="secondary" size="sm" [disabled]="disabled()" (click)="openPicker()">
|
||||
{{ (value() ? 'adminCategories.replaceImage' : 'adminCategories.chooseImage') | translate }}
|
||||
</app-button>
|
||||
@if (value()) {
|
||||
<app-button variant="ghost" size="sm" [disabled]="disabled()" (click)="remove()">{{ 'common.remove' | translate }}</app-button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-media-picker [open]="pickerOpen" (selected)="onPicked($event)" (closed)="pickerOpen = false" />
|
||||
34
src/app/shared/ui/image-field/image-field.component.scss
Normal file
34
src/app/shared/ui/image-field/image-field.component.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-field {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.image-field__thumb {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color, #d3dad9);
|
||||
object-fit: cover;
|
||||
background: #fbfcfc;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.image-field__thumb--empty {
|
||||
background: repeating-conic-gradient(#f0f2f1 0% 25%, #fbfcfc 0% 50%) 0 0 / 12px 12px;
|
||||
}
|
||||
|
||||
.image-field__controls {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.image-field__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
41
src/app/shared/ui/image-field/image-field.component.ts
Normal file
41
src/app/shared/ui/image-field/image-field.component.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
||||
import { MediaAsset } from '../../../core/media/models/media-asset.model';
|
||||
import { InputComponent } from '../input/input.component';
|
||||
import { ButtonComponent } from '../button/button.component';
|
||||
import { MediaPickerComponent } from '../../media/media-picker/media-picker.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-image-field',
|
||||
standalone: true,
|
||||
imports: [FormsModule, TranslatePipe, InputComponent, ButtonComponent, MediaPickerComponent],
|
||||
templateUrl: './image-field.component.html',
|
||||
styleUrl: './image-field.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ImageFieldComponent {
|
||||
readonly value = input<string | null | undefined>('');
|
||||
readonly disabled = input(false);
|
||||
|
||||
readonly valueChange = output<string>();
|
||||
|
||||
protected pickerOpen = false;
|
||||
|
||||
protected setValue(next: string): void {
|
||||
this.valueChange.emit(next);
|
||||
}
|
||||
|
||||
protected openPicker(): void {
|
||||
this.pickerOpen = true;
|
||||
}
|
||||
|
||||
protected onPicked(asset: MediaAsset): void {
|
||||
this.valueChange.emit(asset.url);
|
||||
this.pickerOpen = false;
|
||||
}
|
||||
|
||||
protected remove(): void {
|
||||
this.valueChange.emit('');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user