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:
sdarbinyan
2026-07-17 15:22:45 +04:00
parent feda0f685f
commit b183410888
12 changed files with 175 additions and 71 deletions

View File

@@ -8,6 +8,8 @@ export interface BrandingConfig {
logoCompactUrl?: UrlString;
faviconUrl: UrlString;
appIconUrl?: UrlString;
socialImageUrl?: UrlString;
galleryUrls?: UrlString[];
supportEmail?: string;
supportPhone?: string;
}

View 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" />

View 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;
}

View 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('');
}
}