Files
indie/frontend/src/app/builder/property-panel/property-panel.component.ts
2026-07-01 01:14:40 +05:45

271 lines
12 KiB
TypeScript

import { Component, OnDestroy } from '@angular/core';
import { NgIf, NgFor, NgSwitch, NgSwitchCase, KeyValuePipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { Subscription } from 'rxjs';
import { BuilderStateService } from '../services/builder-state.service';
import { ComponentItem, getComponentLabel } from '../../core/models/component';
@Component({
selector: 'app-property-panel',
standalone: true,
imports: [
NgIf, NgFor, NgSwitch, NgSwitchCase, KeyValuePipe, FormsModule,
MatExpansionModule, MatFormFieldModule, MatInputModule, MatSelectModule,
MatButtonModule, MatIconModule, MatSlideToggleModule,
],
template: `
<div class="p-3 h-full overflow-y-auto">
<h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-3 px-2">Properties</h3>
<div *ngIf="!component" class="flex flex-col items-center justify-center h-40 text-gray-400">
<mat-icon style="font-size: 36px; width: 36px; height: 36px;" class="mb-2">tune</mat-icon>
<p class="text-xs">Select a component to edit</p>
</div>
<div *ngIf="component">
<div class="mb-4 px-2">
<p class="text-sm font-medium text-gray-700">{{ componentLabel }}</p>
<p class="text-xs text-gray-400">{{ component.type }}</p>
</div>
<mat-accordion>
<mat-expansion-panel expanded>
<mat-expansion-panel-header>
<mat-panel-title class="text-sm font-medium">Content</mat-panel-title>
</mat-expansion-panel-header>
<ng-container [ngSwitch]="component.type">
<div *ngSwitchCase="'HEADING'" class="space-y-3">
<mat-form-field appearance="outline">
<mat-label>Text</mat-label>
<input matInput [(ngModel)]="component.config['text']" (ngModelChange)="onConfigChange()" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Level</mat-label>
<mat-select [(value)]="component.config['level']" (selectionChange)="onConfigChange()">
<mat-option value="h1">H1</mat-option>
<mat-option value="h2">H2</mat-option>
<mat-option value="h3">H3</mat-option>
<mat-option value="h4">H4</mat-option>
<mat-option value="h5">H5</mat-option>
<mat-option value="h6">H6</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngSwitchCase="'TEXT'" class="space-y-3">
<mat-form-field appearance="outline">
<mat-label>Content</mat-label>
<textarea matInput rows="4" [(ngModel)]="component.config['content']" (ngModelChange)="onConfigChange()"></textarea>
</mat-form-field>
</div>
<div *ngSwitchCase="'IMAGE'" class="space-y-3">
<mat-form-field appearance="outline">
<mat-label>Image URL</mat-label>
<input matInput [(ngModel)]="component.config['src']" (ngModelChange)="onConfigChange()" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Alt Text</mat-label>
<input matInput [(ngModel)]="component.config['alt']" (ngModelChange)="onConfigChange()" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Link To (optional)</mat-label>
<input matInput [(ngModel)]="component.config['linkTo']" (ngModelChange)="onConfigChange()" />
</mat-form-field>
</div>
<div *ngSwitchCase="'BUTTON'" class="space-y-3">
<mat-form-field appearance="outline">
<mat-label>Button Text</mat-label>
<input matInput [(ngModel)]="component.config['text']" (ngModelChange)="onConfigChange()" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Link</mat-label>
<input matInput [(ngModel)]="component.config['link']" (ngModelChange)="onConfigChange()" />
</mat-form-field>
</div>
<div *ngSwitchCase="'DIVIDER'" class="space-y-3">
<mat-form-field appearance="outline">
<mat-label>Style</mat-label>
<mat-select [(value)]="component.config['style']" (selectionChange)="onConfigChange()">
<mat-option value="solid">Solid</mat-option>
<mat-option value="dashed">Dashed</mat-option>
<mat-option value="dotted">Dotted</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngSwitchCase="'VIDEO'" class="space-y-3">
<mat-form-field appearance="outline">
<mat-label>Video URL</mat-label>
<input matInput [(ngModel)]="component.config['src']" (ngModelChange)="onConfigChange()" />
</mat-form-field>
<div class="flex items-center gap-2">
<mat-slide-toggle [(ngModel)]="component.config['autoplay']" (ngModelChange)="onConfigChange()">
Autoplay
</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="component.config['controls']" (ngModelChange)="onConfigChange()">
Controls
</mat-slide-toggle>
</div>
</div>
<div *ngSwitchCase="'CONTACT_FORM'" class="space-y-3">
<mat-form-field appearance="outline">
<mat-label>Submit Button Text</mat-label>
<input matInput [(ngModel)]="component.config['submitText']" (ngModelChange)="onConfigChange()" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Email To</mat-label>
<input matInput [(ngModel)]="component.config['emailTo']" (ngModelChange)="onConfigChange()" />
</mat-form-field>
</div>
<div *ngSwitchCase="'MAP'" class="space-y-3">
<mat-form-field appearance="outline">
<mat-label>Address</mat-label>
<input matInput [(ngModel)]="component.config['address']" (ngModelChange)="onConfigChange()" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Zoom Level</mat-label>
<mat-select [(value)]="component.config['zoom']" (selectionChange)="onConfigChange()">
<mat-option *ngFor="let z of [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]" [value]="z">{{ z }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngSwitchCase="'GALLERY'" class="space-y-3">
<div *ngFor="let img of getImages(); let i = index" class="flex items-center gap-2 p-2 border rounded">
<span class="text-xs truncate flex-1">{{ img.src }}</span>
<button mat-icon-button type="button" (click)="removeImage(i)" class="w-6 h-6 flex items-center justify-center">
<mat-icon style="font-size: 14px;">close</mat-icon>
</button>
</div>
<div class="flex gap-2">
<mat-form-field appearance="outline" class="flex-1">
<mat-label>Image URL</mat-label>
<input matInput [(ngModel)]="newImageUrl" placeholder="https://..." (keydown.enter)="addImage()" />
</mat-form-field>
<button mat-stroked-button type="button" (click)="addImage()">Add</button>
</div>
<p *ngIf="getImages().length === 0" class="text-xs text-gray-400 px-1">
No images. Add image URLs above.
</p>
</div>
<div *ngSwitchCase="'CUSTOM_HTML'" class="space-y-3">
<mat-form-field appearance="outline">
<mat-label>HTML</mat-label>
<textarea matInput rows="6" [(ngModel)]="component.config['html']" (ngModelChange)="onConfigChange()"></textarea>
</mat-form-field>
</div>
</ng-container>
</mat-expansion-panel>
<mat-expansion-panel expanded>
<mat-expansion-panel-header>
<mat-panel-title class="text-sm font-medium">Styles</mat-panel-title>
</mat-expansion-panel-header>
<div class="space-y-3" *ngIf="component">
<div *ngFor="let entry of styleKeys">
<mat-form-field appearance="outline">
<mat-label>{{ formatKey(entry) }}</mat-label>
<input matInput [value]="component.styles[entry]"
(input)="onStyleChange(entry, $any($event.target).value)" />
</mat-form-field>
</div>
</div>
</mat-expansion-panel>
</mat-accordion>
<div class="mt-4 px-2">
<button mat-stroked-button color="warn" size="small" (click)="removeComponent()" class="w-full">
<mat-icon class="mr-1">delete</mat-icon>
Remove Component
</button>
</div>
</div>
</div>
`,
})
export class PropertyPanelComponent implements OnDestroy {
private sub: Subscription;
component: ComponentItem | null = null;
newImageUrl = '';
constructor(private state: BuilderStateService) {
this.sub = this.state.selectedComponentId$.subscribe(() => {
this.component = this.state.selectedComponent ? { ...this.state.selectedComponent } : null;
this.newImageUrl = '';
});
}
get componentLabel(): string {
return this.component ? getComponentLabel(this.component.type) : '';
}
get styleKeys(): string[] {
return this.component ? Object.keys(this.component.styles) : [];
}
formatKey(key: string): string {
return key.replace(/[A-Z]/g, c => ' ' + c.toLowerCase()).replace(/^./, s => s.toUpperCase());
}
getImages(): { src: string; alt?: string }[] {
return (this.component?.config['images'] as any[]) || [];
}
addImage(): void {
const url = this.newImageUrl?.trim();
if (!url || !this.component) return;
const images = [...this.getImages(), { src: url, alt: '' }];
this.component.config['images'] = images;
this.onConfigChange();
this.newImageUrl = '';
}
removeImage(index: number): void {
if (!this.component) return;
const images = this.getImages().filter((_, i) => i !== index);
this.component.config['images'] = images;
this.onConfigChange();
}
onConfigChange(): void {
if (this.component) {
this.state.updateComponent(this.component.id, { config: { ...this.component.config } });
}
}
onStyleChange(key: string, value: string): void {
if (this.component) {
const updatedStyles = { ...this.component.styles, [key]: value };
this.component = { ...this.component, styles: updatedStyles };
this.state.updateComponent(this.component.id, { styles: updatedStyles });
}
}
removeComponent(): void {
if (this.component) {
this.state.removeComponent(this.component.id);
}
}
ngOnDestroy(): void {
this.sub?.unsubscribe();
}
}