develop #8

Merged
rajukottedi merged 22 commits from develop into master 2026-02-16 10:40:47 +05:30
7 changed files with 211 additions and 16 deletions
Showing only changes of commit 1806c6fb1f - Show all commits

View File

@ -5,6 +5,7 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { DynamicPopupComponent } from '../../shared/dynamic-popup/dynamic-popup'; import { DynamicPopupComponent } from '../../shared/dynamic-popup/dynamic-popup';
import { DynamicFormConfig } from '../../shared/dynamic-form/dynamic-form-config'; import { DynamicFormConfig } from '../../shared/dynamic-form/dynamic-form-config';
import { ProjectDetailPopupComponent } from './project-detail-popup/project-detail-popup'; import { ProjectDetailPopupComponent } from './project-detail-popup/project-detail-popup';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog';
import { AdminQuery } from '../state/admin.query'; import { AdminQuery } from '../state/admin.query';
import { AdminStateService } from '../state/admin-state.service'; import { AdminStateService } from '../state/admin-state.service';
import { AdminService } from '../services/admin.service'; import { AdminService } from '../services/admin.service';
@ -185,20 +186,33 @@ export class Projects implements OnInit, OnDestroy {
} }
deleteProject(project: IProject): void { deleteProject(project: IProject): void {
if (!confirm(`Are you sure you want to delete "${project.name}"?`)) return; const ref = this.dialog.open(ConfirmDialogComponent, {
data: {
title: 'Delete Project',
message: `Are you sure you want to delete "${project.name}"? This action cannot be undone.`
},
panelClass: 'dark-popup-panel',
width: '420px'
});
this.adminService.deleteProject(project.projectId) ref.afterClosed()
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(() => { .subscribe((confirmed: boolean) => {
const allProjects = this.adminQuery.getProjects(); if (!confirmed) return;
const updatedList = (allProjects?.projects ?? []).filter(p => p.projectId !== project.projectId);
const categories = [...new Set(updatedList.flatMap(p => p.categories ?? []))];
this.adminState.updateProjects({ this.adminService.deleteProject(project.projectId)
projects: updatedList, .pipe(takeUntil(this.destroy$))
projectsCategories: categories.length ? categories : [] .subscribe(() => {
}); const allProjects = this.adminQuery.getProjects();
this.filter = 'All'; const updatedList = (allProjects?.projects ?? []).filter(p => p.projectId !== project.projectId);
const categories = [...new Set(updatedList.flatMap(p => p.categories ?? []))];
this.adminState.updateProjects({
projects: updatedList,
projectsCategories: categories.length ? categories : []
});
this.filter = 'All';
});
}); });
} }

View File

@ -0,0 +1,15 @@
<div class="confirm-dialog" role="alertdialog">
<div class="confirm-header">
<i class="fa-solid fa-triangle-exclamation warn-icon"></i>
<h3>{{ title }}</h3>
</div>
<p class="confirm-message">{{ message }}</p>
<div class="confirm-actions">
<button class="btn btn-cancel" (click)="onCancel()">{{ cancelLabel }}</button>
<button class="btn" [class.btn-warn]="isWarn" [class.btn-primary]="!isWarn" (click)="onConfirm()">
{{ confirmLabel }}
</button>
</div>
</div>

View File

@ -0,0 +1,86 @@
.confirm-dialog {
padding: 24px;
background: var(--eerie-black-2, #1e1e1e);
border: 1px solid var(--jet, #383838);
border-radius: 14px;
color: var(--white-1, #fff);
min-width: 320px;
max-width: 420px;
}
.confirm-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
h3 {
margin: 0;
font-size: 1.15rem;
color: var(--white-1, #fff);
}
}
.warn-icon {
color: #ff6b6b;
font-size: 1.2rem;
}
.confirm-message {
margin: 0 0 20px;
font-size: 0.92rem;
line-height: 1.5;
color: var(--white-2, #ddd);
}
.confirm-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.btn {
padding: 8px 18px;
border: none;
border-radius: 8px;
font-size: 0.88rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease, transform 0.1s ease;
&:active {
transform: scale(0.97);
}
}
.btn-cancel {
background: rgba(255, 255, 255, 0.06);
color: var(--light-gray-70, #aaa);
border: 1px solid rgba(255, 255, 255, 0.1);
&:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--white-1, #fff);
}
}
.btn-warn {
background: rgba(255, 70, 70, 0.15);
color: #ff6b6b;
border: 1px solid rgba(255, 70, 70, 0.25);
&:hover {
background: rgba(255, 70, 70, 0.25);
color: #ff4444;
}
}
.btn-primary {
background: rgba(227, 179, 65, 0.15);
color: var(--orange-yellow-crayola, #e3b341);
border: 1px solid rgba(227, 179, 65, 0.25);
&:hover {
background: rgba(227, 179, 65, 0.25);
}
}

View File

@ -0,0 +1,50 @@
import { Component, inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
export interface ConfirmDialogData {
title?: string;
message: string;
confirmLabel?: string;
cancelLabel?: string;
confirmColor?: 'warn' | 'primary' | 'accent';
}
@Component({
selector: 'app-confirm-dialog',
standalone: true,
imports: [MatDialogModule],
templateUrl: './confirm-dialog.html',
styleUrls: ['./confirm-dialog.scss']
})
export class ConfirmDialogComponent {
private dialogRef = inject(MatDialogRef<ConfirmDialogComponent>);
data: ConfirmDialogData = inject(MAT_DIALOG_DATA);
get title(): string {
return this.data.title ?? 'Confirm';
}
get message(): string {
return this.data.message;
}
get confirmLabel(): string {
return this.data.confirmLabel ?? 'Delete';
}
get cancelLabel(): string {
return this.data.cancelLabel ?? 'Cancel';
}
get isWarn(): boolean {
return (this.data.confirmColor ?? 'warn') === 'warn';
}
onCancel(): void {
this.dialogRef.close(false);
}
onConfirm(): void {
this.dialogRef.close(true);
}
}

View File

@ -6,7 +6,7 @@
@if (f.type === 'text') { @if (f.type === 'text') {
<mat-form-field appearance="outline" class="full-width"> <mat-form-field appearance="outline" class="full-width">
<mat-label>{{ f.label }}</mat-label> <mat-label>{{ f.label }}</mat-label>
<input matInput [id]="f.name" [formControlName]="f.name" [placeholder]="f.placeholder || ''" /> <input matInput [id]="f.name" [formControlName]="f.name" />
<mat-hint *ngIf="f.placeholder">{{ f.placeholder }}</mat-hint> <mat-hint *ngIf="f.placeholder">{{ f.placeholder }}</mat-hint>
<mat-error *ngIf="form.get(f.name)?.invalid && (form.get(f.name)?.touched || form.get(f.name)?.dirty)"> <mat-error *ngIf="form.get(f.name)?.invalid && (form.get(f.name)?.touched || form.get(f.name)?.dirty)">
This field is required. This field is required.
@ -17,7 +17,7 @@
@if (f.type === 'textarea') { @if (f.type === 'textarea') {
<mat-form-field appearance="outline" class="full-width"> <mat-form-field appearance="outline" class="full-width">
<mat-label>{{ f.label }}</mat-label> <mat-label>{{ f.label }}</mat-label>
<textarea matInput [id]="f.name" cdkTextareaAutosize cdkAutosizeMinRows="3" cdkAutosizeMaxRows="6" [formControlName]="f.name" [placeholder]="f.placeholder || ''"></textarea> <textarea matInput [id]="f.name" cdkTextareaAutosize cdkAutosizeMinRows="3" cdkAutosizeMaxRows="6" [formControlName]="f.name"></textarea>
<mat-hint *ngIf="f.placeholder">{{ f.placeholder }}</mat-hint> <mat-hint *ngIf="f.placeholder">{{ f.placeholder }}</mat-hint>
<mat-error *ngIf="form.get(f.name)?.invalid && (form.get(f.name)?.touched || form.get(f.name)?.dirty)"> <mat-error *ngIf="form.get(f.name)?.invalid && (form.get(f.name)?.touched || form.get(f.name)?.dirty)">
This field is required. This field is required.

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Component, OnInit, Input, Output, EventEmitter, inject } from '@angular/core';
import { ReactiveFormsModule, FormGroup, FormControl, Validators, FormArray, AbstractControl } from '@angular/forms'; import { ReactiveFormsModule, FormGroup, FormControl, Validators, FormArray, AbstractControl } from '@angular/forms';
import { DynamicFormConfig } from './dynamic-form-config'; import { DynamicFormConfig } from './dynamic-form-config';
import { DynamicField } from './dynamic-field'; import { DynamicField } from './dynamic-field';
@ -9,7 +9,9 @@ import { MatSelectModule } from '@angular/material/select';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { provideNativeDateAdapter } from '@angular/material/core'; import { provideNativeDateAdapter } from '@angular/material/core';
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog';
@Component({ @Component({
selector: "app-dynamic-form", selector: "app-dynamic-form",
@ -17,7 +19,7 @@ import { provideNativeDateAdapter } from '@angular/material/core';
standalone: true, standalone: true,
styleUrls: ['./dynamic-form.scss'], styleUrls: ['./dynamic-form.scss'],
providers: [provideNativeDateAdapter()], providers: [provideNativeDateAdapter()],
imports: [ReactiveFormsModule, CommonModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatButtonModule, MatIconModule, MatDatepickerModule] imports: [ReactiveFormsModule, CommonModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatButtonModule, MatIconModule, MatDatepickerModule, MatDialogModule]
}) })
export class DynamicFormComponent implements OnInit { export class DynamicFormComponent implements OnInit {
@Input() config!: DynamicFormConfig; @Input() config!: DynamicFormConfig;
@ -25,6 +27,8 @@ export class DynamicFormComponent implements OnInit {
@Output() formBuilt = new EventEmitter<FormGroup>(); @Output() formBuilt = new EventEmitter<FormGroup>();
private dialog = inject(MatDialog);
form!: FormGroup; form!: FormGroup;
ngOnInit() { ngOnInit() {
@ -71,7 +75,21 @@ addArrayItem(field: DynamicField) {
removeArrayItem(field: DynamicField, index: number) { removeArrayItem(field: DynamicField, index: number) {
const array = this.form.get(field.name) as FormArray; const array = this.form.get(field.name) as FormArray;
array.removeAt(index);
this.dialog.open(ConfirmDialogComponent, {
data: {
title: 'Remove Item',
message: `Are you sure you want to remove this ${field.label?.toLowerCase() ?? 'item'}?`,
confirmLabel: 'Remove',
confirmColor: 'warn'
},
panelClass: 'dark-popup-panel',
width: '400px'
}).afterClosed().subscribe((confirmed: boolean) => {
if (confirmed) {
array.removeAt(index);
}
});
} }
getArrayControls(name: string): AbstractControl[] { getArrayControls(name: string): AbstractControl[] {

View File

@ -409,6 +409,18 @@
opacity: 1 !important; opacity: 1 !important;
} }
/* Keep required asterisk inline with label */
.mat-mdc-floating-label {
white-space: nowrap !important;
overflow: visible !important;
display: inline-flex !important;
align-items: baseline !important;
}
.mat-mdc-form-field-required-marker {
display: inline !important;
}
/* input / textarea text */ /* input / textarea text */
.mat-form-field, .mat-form-field,
.mat-form-field .mat-input-element, .mat-form-field .mat-input-element,