Skip to content

Commit d8ae7b5

Browse files
feat: Introduce Veil management page with form, item, card, and modal components.
1 parent 6ce135a commit d8ae7b5

10 files changed

Lines changed: 722 additions & 317 deletions

File tree

.agent/rules/angular-signals.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
trigger: model_decision
3-
description: ./apps/admin-panel/src/**/
3+
description: ./frontend/src/**/
44
globs: frontend/**/
55
---
66

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
2+
import { CommonModule, NgOptimizedImage } from '@angular/common';
3+
import { Veil } from '../../../entities/veil/veil.model';
4+
5+
@Component({
6+
selector: 'app-veil-card',
7+
standalone: true,
8+
imports: [CommonModule, NgOptimizedImage],
9+
changeDetection: ChangeDetectionStrategy.OnPush,
10+
template: `
11+
<div class="bg-white rounded-2xl p-6 shadow-[0_4px_6px_-1px_rgba(0,0,0,0.05)] hover:shadow-[0_4px_20px_-2px_rgba(0,0,0,0.05)] transition-all duration-300 group relative reveal-item" [style.animation-delay.ms]="index * 100">
12+
13+
<!-- Top Section: Image & Price -->
14+
<div class="flex justify-between items-start mb-6 relative">
15+
<div class="w-20 h-20 rounded-full overflow-hidden border-2 border-white shadow-md cursor-pointer" (click)="onViewImage()">
16+
<img [src]="safeImageUrl" [alt]="veil.name" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
17+
</div>
18+
<span class="font-serif text-xl font-bold text-gray-900">{{ veil.price }} TJS</span>
19+
<!-- Decorative Blur Effect -->
20+
<div class="absolute -top-4 -right-4 w-24 h-24 bg-primary/5 rounded-full blur-2xl pointer-events-none"></div>
21+
</div>
22+
23+
<!-- Title & SKU -->
24+
<div class="mb-6">
25+
<h3 class="text-2xl font-serif text-gray-900 mb-1">{{ veil.name }}</h3>
26+
<p class="text-xs tracking-wider text-gray-500 uppercase font-medium">{{ veil.sku }}</p>
27+
</div>
28+
29+
<!-- Attributes Box -->
30+
<div class="bg-gray-50 rounded-xl p-5 mb-6 space-y-3">
31+
<div class="flex justify-between items-center text-sm border-b border-gray-100 pb-2 last:border-0 last:pb-0">
32+
<span class="text-gray-500">Silhouette</span>
33+
<span class="font-semibold font-serif text-gray-900">{{ veil.silhouette }}</span>
34+
</div>
35+
<div class="flex justify-between items-center text-sm border-b border-gray-100 pb-2 last:border-0 last:pb-0">
36+
<span class="text-gray-500">Neckline</span>
37+
<span class="font-semibold font-serif text-gray-900">{{ veil.neckline }}</span>
38+
</div>
39+
<div class="flex justify-between items-center text-sm border-b border-gray-100 pb-2 last:border-0 last:pb-0">
40+
<span class="text-gray-500">Fabric</span>
41+
<span class="font-semibold font-serif text-gray-900">{{ veil.fabric }}</span>
42+
</div>
43+
<div class="flex justify-between items-center text-sm">
44+
<span class="text-gray-500">Train Length</span>
45+
<span class="font-semibold font-serif text-gray-900">{{ veil.trainLength }}</span>
46+
</div>
47+
</div>
48+
49+
<!-- Footer: Status & Action -->
50+
<div class="flex items-center justify-between pt-2">
51+
<div class="flex items-center gap-2">
52+
<span class="w-2.5 h-2.5 rounded-full" [class]="veil.stock > 0 ? 'bg-green-500 ' + (veil.stock > 1 ? 'animate-pulse' : '') : 'bg-yellow-500'"></span>
53+
<span class="text-sm font-medium text-gray-900">
54+
<ng-container *ngIf="veil.stock > 0; else outStock">
55+
<ng-container i18n="@@veilAvailable">{{ veil.stock }} Available</ng-container>
56+
</ng-container>
57+
<ng-template #outStock>
58+
<ng-container i18n="@@veilOutOfStock">Out of Stock</ng-container>
59+
</ng-template>
60+
</span>
61+
</div>
62+
<button (click)="onEdit()" class="flex items-center gap-1 text-primary hover:text-primary-hover transition-colors text-sm font-medium group-hover:translate-x-1 transition-transform">
63+
<span class="material-symbols-outlined text-base">edit</span>
64+
<span i18n="@@veilAdminEdit">Edit</span>
65+
</button>
66+
</div>
67+
68+
</div>
69+
`
70+
})
71+
export class VeilCardComponent {
72+
@Input({ required: true }) veil!: Veil;
73+
@Input() index: number = 0;
74+
@Output() edit = new EventEmitter<Veil>();
75+
@Output() viewImage = new EventEmitter<string>();
76+
77+
get safeImageUrl(): string {
78+
return this.veil.images && this.veil.images.length > 0 ? this.veil.images[0] : 'assets/placeholder-gown.png';
79+
}
80+
81+
onEdit() {
82+
this.edit.emit(this.veil);
83+
}
84+
85+
onViewImage() {
86+
this.viewImage.emit(this.safeImageUrl);
87+
}
88+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<div class="fixed inset-0 z-[120] overflow-y-auto" role="dialog" aria-modal="true">
2+
<!-- Backdrop -->
3+
<div class="fixed inset-0 bg-black/70 backdrop-blur-sm transition-opacity animate-fade-in" (click)="onCancel()"></div>
4+
5+
<div class="flex min-h-screen items-center justify-center p-4">
6+
<div class="relative w-full max-w-3xl transform overflow-hidden rounded-2xl bg-white text-left shadow-2xl transition-all border border-gold/10 animate-slide-up">
7+
8+
<div class="bg-gray-50 px-6 py-4 border-b border-gray-100 flex justify-between items-center">
9+
<h3 class="text-xl font-serif font-bold text-gray-900">
10+
@if(!isEditMode()) { <span i18n="@@veilModalTitleNew">Add New Gown</span> } @else { <span i18n="@@veilModalTitleEdit">Edit Gown Details</span> }
11+
</h3>
12+
<button (click)="onCancel()" class="text-gray-400 hover:text-gray-500 transition-colors">
13+
<span class="material-symbols-outlined">close</span>
14+
</button>
15+
</div>
16+
17+
<div class="px-6 py-6 grid grid-cols-1 md:grid-cols-3 gap-x-8" [formGroup]="veilForm">
18+
<!-- Image Uploader -->
19+
<div class="md:col-span-1 space-y-2 mb-6 md:mb-0">
20+
<label class="block text-sm font-medium text-gray-700">Gown Image</label>
21+
<div (click)="fileInput.click()" class="aspect-square w-full rounded-xl border-2 border-dashed border-gray-300 flex flex-col items-center justify-center relative group cursor-pointer hover:border-gold transition-colors bg-gray-50">
22+
@if (previewImage()) {
23+
<img [src]="previewImage()" alt="Gown preview" class="w-full h-full object-cover rounded-lg absolute inset-0">
24+
<div class="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-lg">
25+
<div class="text-center text-white">
26+
<span class="material-symbols-outlined text-3xl">edit</span>
27+
<p class="text-xs mt-1 font-semibold">Change Image</p>
28+
</div>
29+
</div>
30+
} @else {
31+
<div class="text-center text-gray-500">
32+
<span class="material-symbols-outlined text-4xl">add_photo_alternate</span>
33+
<p class="text-xs mt-2 font-semibold">Click to upload</p>
34+
</div>
35+
}
36+
</div>
37+
<input #fileInput type="file" class="hidden" (change)="onFileSelected($event)" accept="image/png, image/jpeg, image/webp">
38+
39+
<div class="mt-4">
40+
<label class="inline-flex items-center cursor-pointer">
41+
<input type="checkbox" formControlName="isAvailable" class="sr-only peer">
42+
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-gold/30 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-gold"></div>
43+
<span class="ms-3 text-sm font-medium text-gray-700">Available for Sale</span>
44+
</label>
45+
</div>
46+
</div>
47+
48+
<!-- Form Fields -->
49+
<div class="md:col-span-2 space-y-5">
50+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
51+
<div class="space-y-2">
52+
<label class="block text-sm font-medium text-gray-700">Name</label>
53+
<input type="text" formControlName="name" class="block w-full rounded-lg border-gray-300 bg-gray-50 text-gray-900 focus:bg-white focus:border-gold focus:ring-gold sm:text-sm p-2.5 transition-all">
54+
</div>
55+
<div class="space-y-2">
56+
<label class="block text-sm font-medium text-gray-700">Category</label>
57+
<select formControlName="category" class="block w-full rounded-lg border-gray-300 bg-gray-50 text-gray-900 focus:bg-white focus:border-gold focus:ring-gold sm:text-sm p-2.5 transition-all">
58+
<option value="Bridal">Bridal</option>
59+
<option value="Evening">Evening</option>
60+
<option value="Accessories">Accessories</option>
61+
</select>
62+
</div>
63+
</div>
64+
65+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
66+
<div class="space-y-2">
67+
<label class="block text-sm font-medium text-gray-700">Price (TJS)</label>
68+
<input type="number" formControlName="price" class="block w-full rounded-lg border-gray-300 bg-gray-50 text-gray-900 focus:bg-white focus:border-gold focus:ring-gold sm:text-sm p-2.5 transition-all">
69+
</div>
70+
<div class="space-y-2">
71+
<label class="block text-sm font-medium text-gray-700">Rental Price (TJS)</label>
72+
<input type="number" formControlName="rentalPrice" class="block w-full rounded-lg border-gray-300 bg-gray-50 text-gray-900 focus:bg-white focus:border-gold focus:ring-gold sm:text-sm p-2.5 transition-all">
73+
</div>
74+
</div>
75+
76+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
77+
<div class="space-y-2">
78+
<label class="block text-sm font-medium text-gray-700">SKU</label>
79+
<input type="text" formControlName="sku" class="block w-full rounded-lg border-gray-300 bg-gray-50 text-gray-900 focus:bg-white focus:border-gold focus:ring-gold sm:text-sm p-2.5 transition-all">
80+
</div>
81+
<div class="space-y-2">
82+
<label class="block text-sm font-medium text-gray-700">Stock Quantity</label>
83+
<input type="number" formControlName="stock" class="block w-full rounded-lg border-gray-300 bg-gray-50 text-gray-900 focus:bg-white focus:border-gold focus:ring-gold sm:text-sm p-2.5 transition-all">
84+
</div>
85+
</div>
86+
87+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
88+
<div class="space-y-2">
89+
<label class="block text-sm font-medium text-gray-700">Silhouette</label>
90+
<input type="text" formControlName="silhouette" class="block w-full rounded-lg border-gray-300 bg-gray-50 text-gray-900 focus:bg-white focus:border-gold focus:ring-gold sm:text-sm p-2.5 transition-all">
91+
</div>
92+
<div class="space-y-2">
93+
<label class="block text-sm font-medium text-gray-700">Neckline</label>
94+
<input type="text" formControlName="neckline" class="block w-full rounded-lg border-gray-300 bg-gray-50 text-gray-900 focus:bg-white focus:border-gold focus:ring-gold sm:text-sm p-2.5 transition-all">
95+
</div>
96+
</div>
97+
98+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
99+
<div class="space-y-2">
100+
<label class="block text-sm font-medium text-gray-700">Fabric</label>
101+
<input type="text" formControlName="fabric" class="block w-full rounded-lg border-gray-300 bg-gray-50 text-gray-900 focus:bg-white focus:border-gold focus:ring-gold sm:text-sm p-2.5 transition-all">
102+
</div>
103+
<div class="space-y-2">
104+
<label class="block text-sm font-medium text-gray-700">Train Length</label>
105+
<input type="text" formControlName="trainLength" class="block w-full rounded-lg border-gray-300 bg-gray-50 text-gray-900 focus:bg-white focus:border-gold focus:ring-gold sm:text-sm p-2.5 transition-all">
106+
</div>
107+
</div>
108+
109+
<div class="space-y-2">
110+
<label class="block text-sm font-medium text-gray-700">Description</label>
111+
<textarea formControlName="description" rows="3" class="block w-full rounded-lg border-gray-300 bg-gray-50 text-gray-900 focus:bg-white focus:border-gold focus:ring-gold sm:text-sm p-2.5 transition-all"></textarea>
112+
</div>
113+
</div>
114+
</div>
115+
116+
<div class="bg-gray-50 px-6 py-4 flex flex-row-reverse border-t border-gray-100 gap-3">
117+
<button (click)="onSubmit()" type="button" [disabled]="veilForm.invalid" class="inline-flex justify-center rounded-lg bg-gold px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-gold/30 hover:bg-gold-dark hover:shadow-gold/50 transition-all transform active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed">
118+
@if(!isEditMode()) { <span i18n="@@veilBtnCreate">Create Gown</span> } @else { <span i18n="@@veilBtnSave">Save Changes</span> }
119+
</button>
120+
<button (click)="onCancel()" type="button" class="inline-flex justify-center rounded-lg bg-white px-4 py-2 text-sm font-semibold text-gray-700 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 transition-all">
121+
Cancel
122+
</button>
123+
</div>
124+
</div>
125+
</div>
126+
</div>
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, inject, signal, OnInit, OnChanges, SimpleChanges } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
4+
import { Veil } from '../../../entities/veil/veil.model';
5+
6+
@Component({
7+
selector: 'app-veil-form',
8+
standalone: true,
9+
imports: [CommonModule, ReactiveFormsModule],
10+
changeDetection: ChangeDetectionStrategy.OnPush,
11+
templateUrl: './veil-form.component.html'
12+
})
13+
export class VeilFormComponent implements OnInit, OnChanges {
14+
@Input() veil: Veil | null = null;
15+
@Output() save = new EventEmitter<{ data: any, file: File | null }>();
16+
@Output() cancel = new EventEmitter<void>();
17+
18+
private fb = inject(FormBuilder);
19+
veilForm!: FormGroup;
20+
selectedFile = signal<File | null>(null);
21+
previewImage = signal<string | null>(null);
22+
isEditMode = signal(false);
23+
24+
constructor() {
25+
this.initForm();
26+
}
27+
28+
ngOnInit() {
29+
if (this.veil) {
30+
this.populateForm(this.veil);
31+
}
32+
}
33+
34+
ngOnChanges(changes: SimpleChanges) {
35+
if (changes['veil'] && changes['veil'].currentValue) {
36+
this.populateForm(changes['veil'].currentValue);
37+
} else if (changes['veil'] && !changes['veil'].currentValue) {
38+
this.resetForm();
39+
}
40+
}
41+
42+
initForm() {
43+
this.veilForm = this.fb.group({
44+
name: ['', Validators.required],
45+
price: [0, [Validators.required, Validators.min(0)]],
46+
rentalPrice: [0, [Validators.required, Validators.min(0)]],
47+
stock: [0, [Validators.required, Validators.min(0)]],
48+
sku: [''],
49+
silhouette: [''],
50+
neckline: [''],
51+
fabric: [''],
52+
trainLength: [''],
53+
category: ['Bridal'],
54+
description: [''],
55+
isAvailable: [true]
56+
});
57+
}
58+
59+
populateForm(veil: Veil) {
60+
this.isEditMode.set(true);
61+
this.previewImage.set(veil.images && veil.images.length > 0 ? veil.images[0] : null);
62+
63+
this.veilForm.patchValue({
64+
name: veil.name,
65+
price: veil.price,
66+
rentalPrice: veil.rentalPrice || 0,
67+
stock: veil.stock,
68+
sku: veil.sku,
69+
silhouette: veil.silhouette,
70+
neckline: veil.neckline,
71+
fabric: veil.fabric,
72+
trainLength: veil.trainLength,
73+
category: veil.category,
74+
description: veil.description || '',
75+
isAvailable: veil.isAvailable
76+
});
77+
}
78+
79+
resetForm() {
80+
this.isEditMode.set(false);
81+
this.selectedFile.set(null);
82+
this.previewImage.set(null);
83+
this.veilForm.reset({
84+
name: '',
85+
price: 0,
86+
rentalPrice: 0,
87+
stock: 0,
88+
category: 'Bridal',
89+
isAvailable: true,
90+
description: ''
91+
});
92+
}
93+
94+
onFileSelected(event: Event) {
95+
const input = event.target as HTMLInputElement;
96+
if (input.files && input.files[0]) {
97+
const file = input.files[0];
98+
this.selectedFile.set(file);
99+
100+
const reader = new FileReader();
101+
reader.onload = (e: any) => {
102+
this.previewImage.set(e.target.result);
103+
};
104+
reader.readAsDataURL(file);
105+
}
106+
}
107+
108+
onSubmit() {
109+
if (this.veilForm.valid) {
110+
this.save.emit({
111+
data: this.veilForm.value,
112+
file: this.selectedFile()
113+
});
114+
}
115+
}
116+
117+
onCancel() {
118+
this.cancel.emit();
119+
}
120+
}

0 commit comments

Comments
 (0)