diff --git a/src/app/admin/projects/projects.ts b/src/app/admin/projects/projects.ts
index 9a1316d..c18f169 100644
--- a/src/app/admin/projects/projects.ts
+++ b/src/app/admin/projects/projects.ts
@@ -5,6 +5,7 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { DynamicPopupComponent } from '../../shared/dynamic-popup/dynamic-popup';
import { DynamicFormConfig } from '../../shared/dynamic-form/dynamic-form-config';
import { ProjectDetailPopupComponent } from './project-detail-popup/project-detail-popup';
+import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog';
import { AdminQuery } from '../state/admin.query';
import { AdminStateService } from '../state/admin-state.service';
import { AdminService } from '../services/admin.service';
@@ -185,20 +186,33 @@ export class Projects implements OnInit, OnDestroy {
}
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$))
- .subscribe(() => {
- const allProjects = this.adminQuery.getProjects();
- const updatedList = (allProjects?.projects ?? []).filter(p => p.projectId !== project.projectId);
- const categories = [...new Set(updatedList.flatMap(p => p.categories ?? []))];
+ .subscribe((confirmed: boolean) => {
+ if (!confirmed) return;
- this.adminState.updateProjects({
- projects: updatedList,
- projectsCategories: categories.length ? categories : []
- });
- this.filter = 'All';
+ this.adminService.deleteProject(project.projectId)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(() => {
+ const allProjects = this.adminQuery.getProjects();
+ 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';
+ });
});
}
diff --git a/src/app/shared/confirm-dialog/confirm-dialog.html b/src/app/shared/confirm-dialog/confirm-dialog.html
new file mode 100644
index 0000000..43c507e
--- /dev/null
+++ b/src/app/shared/confirm-dialog/confirm-dialog.html
@@ -0,0 +1,15 @@
+
+
+
+
{{ message }}
+
+
+
+
+
+
diff --git a/src/app/shared/confirm-dialog/confirm-dialog.scss b/src/app/shared/confirm-dialog/confirm-dialog.scss
new file mode 100644
index 0000000..a5ed86b
--- /dev/null
+++ b/src/app/shared/confirm-dialog/confirm-dialog.scss
@@ -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);
+ }
+}
diff --git a/src/app/shared/confirm-dialog/confirm-dialog.ts b/src/app/shared/confirm-dialog/confirm-dialog.ts
new file mode 100644
index 0000000..edd0985
--- /dev/null
+++ b/src/app/shared/confirm-dialog/confirm-dialog.ts
@@ -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);
+ 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);
+ }
+}
diff --git a/src/app/shared/dynamic-form/dynamic-form.html b/src/app/shared/dynamic-form/dynamic-form.html
index 5fbda29..d5dcc35 100644
--- a/src/app/shared/dynamic-form/dynamic-form.html
+++ b/src/app/shared/dynamic-form/dynamic-form.html
@@ -6,7 +6,7 @@
@if (f.type === 'text') {
{{ f.label }}
-
+
{{ f.placeholder }}
This field is required.
@@ -17,7 +17,7 @@
@if (f.type === 'textarea') {
{{ f.label }}
-
+
{{ f.placeholder }}
This field is required.
diff --git a/src/app/shared/dynamic-form/dynamic-form.ts b/src/app/shared/dynamic-form/dynamic-form.ts
index 4d87056..5e34afc 100644
--- a/src/app/shared/dynamic-form/dynamic-form.ts
+++ b/src/app/shared/dynamic-form/dynamic-form.ts
@@ -1,5 +1,5 @@
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 { DynamicFormConfig } from './dynamic-form-config';
import { DynamicField } from './dynamic-field';
@@ -9,7 +9,9 @@ import { MatSelectModule } from '@angular/material/select';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatDatepickerModule } from '@angular/material/datepicker';
+import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { provideNativeDateAdapter } from '@angular/material/core';
+import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog';
@Component({
selector: "app-dynamic-form",
@@ -17,7 +19,7 @@ import { provideNativeDateAdapter } from '@angular/material/core';
standalone: true,
styleUrls: ['./dynamic-form.scss'],
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 {
@Input() config!: DynamicFormConfig;
@@ -25,6 +27,8 @@ export class DynamicFormComponent implements OnInit {
@Output() formBuilt = new EventEmitter();
+ private dialog = inject(MatDialog);
+
form!: FormGroup;
ngOnInit() {
@@ -71,7 +75,21 @@ addArrayItem(field: DynamicField) {
removeArrayItem(field: DynamicField, index: number) {
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[] {
diff --git a/src/styles.scss b/src/styles.scss
index 5ee9f7f..bfc1d0a 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -409,6 +409,18 @@
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 */
.mat-form-field,
.mat-form-field .mat-input-element,