Further updates on 03-11-2025

This commit is contained in:
2025-11-03 13:46:43 +05:30
parent 91cb073c78
commit 434d95eeaf
87 changed files with 17782 additions and 3451 deletions
@@ -0,0 +1,535 @@
/* Testimonial Form Layout */
.testimonial-form-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.testimonial-form-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1000px;
}
/* Header Section */
.testimonial-form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
gap: 20px;
}
.header-left {
flex: 1;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 4px 0;
letter-spacing: -0.5px;
}
.page-subtitle {
font-size: 14px;
color: #6b7280;
margin: 0;
}
.header-actions {
display: flex;
gap: 12px;
}
/* Buttons */
.btn-secondary,
.btn-cancel,
.btn-submit {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-secondary {
background: white;
color: #374151;
border: 1px solid #e5e7eb;
}
.btn-secondary:hover:not(:disabled) {
background: #f9fafb;
border-color: #d1d5db;
}
.btn-secondary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-cancel {
background: #f3f4f6;
color: #6b7280;
}
.btn-cancel:hover:not(:disabled) {
background: #e5e7eb;
}
.btn-cancel:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-submit {
background: #3b82f6;
color: white;
}
.btn-submit:hover:not(:disabled) {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.btn-submit:disabled {
background: #9ca3af;
cursor: not-allowed;
opacity: 0.6;
transform: none;
}
/* Loading State */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
}
.spinner {
font-size: 48px;
color: #3b82f6;
margin-bottom: 16px;
}
.spinner i {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loading-state p {
font-size: 14px;
color: #6b7280;
margin: 0;
}
/* Form */
.testimonial-form {
display: flex;
flex-direction: column;
gap: 24px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Form Card */
.form-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
gap: 10px;
padding: 20px 24px;
background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
border-bottom: 1px solid #e5e7eb;
}
.card-header i {
font-size: 18px;
color: #3b82f6;
}
.card-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
}
.card-body {
padding: 32px;
}
/* Form Row */
.form-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 20px;
}
/* Form Groups */
.form-group {
margin-bottom: 24px;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-group label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 8px;
}
.form-group label i {
color: #6b7280;
font-size: 14px;
}
.required {
color: #dc2626;
margin-left: 4px;
}
/* Form Inputs */
.form-input,
.form-select,
.form-textarea {
width: 100%;
padding: 12px 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
color: #1a1a1a;
transition: all 0.2s ease;
background: white;
font-family: inherit;
}
.form-input:focus,
.form-select:focus,
.form-textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input::placeholder,
.form-textarea::placeholder {
color: #9ca3af;
}
.form-textarea {
resize: vertical;
min-height: 100px;
line-height: 1.6;
}
.form-select {
cursor: pointer;
}
.input-error {
border-color: #dc2626;
}
.input-error:focus {
border-color: #dc2626;
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
}
/* Form Hints */
.form-hint {
display: block;
font-size: 12px;
color: #6b7280;
margin-top: 6px;
}
/* Character Counter */
.char-counter {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
margin-top: 6px;
}
.char-counter span:first-child {
font-weight: 600;
color: #374151;
}
.text-warning {
color: #f59e0b !important;
}
.text-muted {
color: #9ca3af;
}
/* Error Messages */
.error-message {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
font-size: 13px;
color: #dc2626;
}
.error-message i {
font-size: 14px;
flex-shrink: 0;
}
/* Checkbox */
.checkbox-wrapper {
position: relative;
}
.checkbox-input {
position: absolute;
opacity: 0;
cursor: pointer;
}
.checkbox-label {
display: flex;
align-items: start;
gap: 12px;
cursor: pointer;
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
transition: all 0.2s ease;
background: white;
}
.checkbox-label:hover {
border-color: #d1d5db;
background: #f9fafb;
}
.checkbox-input:checked + .checkbox-label {
border-color: #3b82f6;
background: #f0f9ff;
}
.checkbox-custom {
position: relative;
width: 20px;
height: 20px;
border: 2px solid #d1d5db;
border-radius: 4px;
flex-shrink: 0;
transition: all 0.2s ease;
background: white;
margin-top: 2px;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom {
background: #3b82f6;
border-color: #3b82f6;
}
.checkbox-input:checked + .checkbox-label .checkbox-custom::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox-text {
display: flex;
flex-direction: column;
gap: 4px;
}
.checkbox-text i {
color: #6b7280;
margin-right: 4px;
}
.checkbox-text strong {
font-size: 14px;
color: #1a1a1a;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
}
.checkbox-text small {
font-size: 12px;
color: #6b7280;
font-weight: normal;
}
/* Form Actions */
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 24px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
position: sticky;
bottom: 20px;
z-index: 10;
}
/* Spinner Animation */
.fa-spin {
animation: spin 1s linear infinite;
}
/* Responsive Design */
@media (max-width: 992px) {
.testimonial-form-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.testimonial-form-header {
flex-direction: column;
align-items: flex-start;
}
.header-actions {
width: 100%;
}
.btn-secondary {
width: 100%;
justify-content: center;
}
.form-row {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.testimonial-form-content {
padding: 20px;
}
.page-title {
font-size: 24px;
}
.card-body {
padding: 24px;
}
.form-actions {
flex-direction: column;
}
.form-actions button {
width: 100%;
justify-content: center;
}
}
@media (max-width: 576px) {
.testimonial-form-content {
padding: 16px;
}
.page-title {
font-size: 20px;
}
.card-header {
padding: 16px 20px;
}
.card-body {
padding: 20px;
}
}
/* Print Styles */
@media print {
.sidebar,
.header-actions,
.form-actions {
display: none;
}
.testimonial-form-content {
margin-left: 0;
width: 100%;
}
}
@@ -0,0 +1,257 @@
<div class="testimonial-form-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<!-- Main Content -->
<div class="testimonial-form-content">
<!-- Header Section -->
<div class="testimonial-form-header">
<div class="header-left">
<h1 class="page-title">{{ isEditMode ? 'Edit Testimonial' : 'Add New Testimonial' }}</h1>
<p class="page-subtitle">{{ isEditMode ? 'Update patient story and information' : 'Share a patient\'s journey and recovery story' }}</p>
</div>
<div class="header-actions">
<button class="btn-secondary" (click)="onCancel()" [disabled]="submitting">
<i class="fas fa-arrow-left"></i>
<span>Back to List</span>
</button>
</div>
</div>
<!-- Loading State -->
<div *ngIf="loading" class="loading-state">
<div class="spinner">
<i class="fas fa-spinner fa-spin"></i>
</div>
<p>Loading testimonial...</p>
</div>
<!-- Form Container -->
<form *ngIf="!loading" [formGroup]="testimonialForm" (ngSubmit)="onSubmit()" class="testimonial-form">
<!-- Patient Information Card -->
<div class="form-card">
<div class="card-header">
<i class="fas fa-user"></i>
<h3>Patient Information</h3>
</div>
<div class="card-body">
<div class="form-row">
<!-- Name -->
<div class="form-group">
<label for="name">
<i class="fas fa-id-card"></i>
Patient Name
<span class="required">*</span>
</label>
<input
type="text"
id="name"
formControlName="name"
class="form-input"
[class.input-error]="testimonialForm.get('name')?.invalid && testimonialForm.get('name')?.touched"
placeholder="Enter patient name">
<div class="char-counter">
<span [class.text-warning]="getCharacterCount('name') > 80">
{{ getCharacterCount('name') }}
</span>
<span class="text-muted">/ {{ getMaxLength('name') }} characters</span>
</div>
<div class="error-message" *ngIf="testimonialForm.get('name')?.invalid && testimonialForm.get('name')?.touched">
<i class="fas fa-exclamation-circle"></i>
<span *ngIf="testimonialForm.get('name')?.errors?.['required']">Name is required</span>
<span *ngIf="testimonialForm.get('name')?.errors?.['maxlength']">Name must not exceed 100 characters</span>
</div>
</div>
<!-- Age -->
<div class="form-group">
<label for="age">
<i class="fas fa-birthday-cake"></i>
Age
<span class="required">*</span>
</label>
<input
type="number"
id="age"
formControlName="age"
class="form-input"
[class.input-error]="testimonialForm.get('age')?.invalid && testimonialForm.get('age')?.touched"
placeholder="Enter age"
min="1"
max="120">
<div class="error-message" *ngIf="testimonialForm.get('age')?.invalid && testimonialForm.get('age')?.touched">
<i class="fas fa-exclamation-circle"></i>
<span *ngIf="testimonialForm.get('age')?.errors?.['required']">Age is required</span>
<span *ngIf="testimonialForm.get('age')?.errors?.['min']">Age must be at least 1</span>
<span *ngIf="testimonialForm.get('age')?.errors?.['max']">Age must not exceed 120</span>
</div>
</div>
</div>
</div>
</div>
<!-- Story Details Card -->
<div class="form-card">
<div class="card-header">
<i class="fas fa-book-open"></i>
<h3>Story Details</h3>
</div>
<div class="card-body">
<!-- Title -->
<div class="form-group">
<label for="title">
<i class="fas fa-heading"></i>
Story Title
<span class="required">*</span>
</label>
<input
type="text"
id="title"
formControlName="title"
class="form-input"
[class.input-error]="testimonialForm.get('title')?.invalid && testimonialForm.get('title')?.touched"
placeholder="e.g., A Journey Through Pain and Hope">
<div class="char-counter">
<span [class.text-warning]="getCharacterCount('title') > 180">
{{ getCharacterCount('title') }}
</span>
<span class="text-muted">/ {{ getMaxLength('title') }} characters</span>
</div>
<div class="error-message" *ngIf="testimonialForm.get('title')?.invalid && testimonialForm.get('title')?.touched">
<i class="fas fa-exclamation-circle"></i>
<span *ngIf="testimonialForm.get('title')?.errors?.['required']">Title is required</span>
<span *ngIf="testimonialForm.get('title')?.errors?.['maxlength']">Title must not exceed 200 characters</span>
</div>
</div>
<!-- Category -->
<div class="form-group">
<label for="category">
<i class="fas fa-tag"></i>
Category
<span class="required">*</span>
</label>
<select
id="category"
formControlName="category"
class="form-select"
[class.input-error]="testimonialForm.get('category')?.invalid && testimonialForm.get('category')?.touched">
<option value="">Select a category</option>
<option *ngFor="let cat of categories" [value]="cat">{{ cat }}</option>
</select>
<div class="error-message" *ngIf="testimonialForm.get('category')?.invalid && testimonialForm.get('category')?.touched">
<i class="fas fa-exclamation-circle"></i>
Category is required
</div>
</div>
<!-- Story -->
<div class="form-group">
<label for="story">
<i class="fas fa-file-medical"></i>
The Journey (Story)
<span class="required">*</span>
</label>
<textarea
id="story"
formControlName="story"
class="form-textarea"
[class.input-error]="testimonialForm.get('story')?.invalid && testimonialForm.get('story')?.touched"
rows="5"
placeholder="Describe the patient's accident and initial condition..."></textarea>
<small class="form-hint">Describe how the accident happened and the injuries sustained</small>
<div class="error-message" *ngIf="testimonialForm.get('story')?.invalid && testimonialForm.get('story')?.touched">
<i class="fas fa-exclamation-circle"></i>
Story is required
</div>
</div>
<!-- Outcome -->
<div class="form-group">
<label for="outcome">
<i class="fas fa-heartbeat"></i>
The Outcome
<span class="required">*</span>
</label>
<textarea
id="outcome"
formControlName="outcome"
class="form-textarea"
[class.input-error]="testimonialForm.get('outcome')?.invalid && testimonialForm.get('outcome')?.touched"
rows="5"
placeholder="Describe the patient's recovery and current status..."></textarea>
<small class="form-hint">Describe the treatment outcome and current condition</small>
<div class="error-message" *ngIf="testimonialForm.get('outcome')?.invalid && testimonialForm.get('outcome')?.touched">
<i class="fas fa-exclamation-circle"></i>
Outcome is required
</div>
</div>
<!-- Impact -->
<div class="form-group">
<label for="impact">
<i class="fas fa-hands-helping"></i>
Community Impact
<span class="required">*</span>
</label>
<textarea
id="impact"
formControlName="impact"
class="form-textarea"
[class.input-error]="testimonialForm.get('impact')?.invalid && testimonialForm.get('impact')?.touched"
rows="4"
placeholder="Describe how this case was supported (funding, programs, etc.)..."></textarea>
<small class="form-hint">Describe funding sources, community support, or program involvement</small>
<div class="error-message" *ngIf="testimonialForm.get('impact')?.invalid && testimonialForm.get('impact')?.touched">
<i class="fas fa-exclamation-circle"></i>
Impact description is required
</div>
</div>
<!-- Active Status -->
<div class="form-group">
<div class="checkbox-wrapper">
<input
type="checkbox"
id="isActive"
formControlName="isActive"
class="checkbox-input">
<label for="isActive" class="checkbox-label">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
<i class="fas fa-eye"></i>
<strong>Active Status</strong>
<small>Check to show this testimonial on the public website</small>
</span>
</label>
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="form-actions">
<button
type="button"
class="btn-cancel"
(click)="onCancel()"
[disabled]="submitting">
<i class="fas fa-times"></i>
<span>Cancel</span>
</button>
<button
type="submit"
class="btn-submit"
[disabled]="testimonialForm.invalid || submitting">
<i class="fas"
[class.fa-spinner]="submitting"
[class.fa-spin]="submitting"
[class.fa-save]="!submitting"></i>
<span>{{ submitting ? 'Saving...' : (isEditMode ? 'Update Testimonial' : 'Create Testimonial') }}</span>
</button>
</div>
</form>
</div>
</div>
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TestimonialFormComponent } from './testimonial-form.component';
describe('TestimonialFormComponent', () => {
let component: TestimonialFormComponent;
let fixture: ComponentFixture<TestimonialFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ TestimonialFormComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TestimonialFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,157 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { TestimonialService } from '../../../service/testimonial.service';
import { Testimonial } from '../../../model/testimonial.model';
@Component({
selector: 'app-testimonial-form',
templateUrl: './testimonial-form.component.html',
styleUrls: ['./testimonial-form.component.css']
})
export class TestimonialFormComponent implements OnInit {
testimonialForm!: FormGroup;
isEditMode: boolean = false;
testimonialId: number | null = null;
loading: boolean = false;
submitting: boolean = false;
categories: string[] = [
'Road Safety',
'Spinal Injury',
'Critical Care',
'Amputation',
'Head Injury',
'Burn Injury',
'Multiple Trauma',
'Pediatric Trauma',
'Other'
];
constructor(
private fb: FormBuilder,
private testimonialService: TestimonialService,
private router: Router,
private route: ActivatedRoute
) {}
ngOnInit(): void {
this.createForm();
this.checkEditMode();
}
createForm(): void {
this.testimonialForm = this.fb.group({
name: ['', [Validators.required, Validators.maxLength(100)]],
age: ['', [Validators.required, Validators.min(1), Validators.max(120)]],
title: ['', [Validators.required, Validators.maxLength(200)]],
story: ['', [Validators.required]],
outcome: ['', [Validators.required]],
impact: ['', [Validators.required]],
category: ['', [Validators.required]],
isActive: [true]
});
}
checkEditMode(): void {
this.route.params.subscribe(params => {
if (params['id']) {
this.isEditMode = true;
this.testimonialId = +params['id'];
this.loadTestimonial(this.testimonialId);
}
});
}
loadTestimonial(id: number): void {
this.loading = true;
this.testimonialService.getTestimonialById(id).subscribe({
next: (testimonial) => {
this.testimonialForm.patchValue({
name: testimonial.name,
age: testimonial.age,
title: testimonial.title,
story: testimonial.story,
outcome: testimonial.outcome,
impact: testimonial.impact,
category: testimonial.category,
isActive: testimonial.isActive
});
this.loading = false;
},
error: (error) => {
console.error('Error loading testimonial:', error);
alert('Failed to load testimonial');
this.router.navigate(['/dashboard/testimonial/list']);
this.loading = false;
}
});
}
onSubmit(): void {
if (this.testimonialForm.invalid) {
this.testimonialForm.markAllAsTouched();
return;
}
this.submitting = true;
const testimonialData = this.prepareFormData();
if (this.isEditMode && this.testimonialId) {
this.testimonialService.updateTestimonial(this.testimonialId, testimonialData).subscribe({
next: () => {
alert('Testimonial updated successfully');
this.router.navigate(['/dashboard/testimonial/list']);
},
error: (error) => {
console.error('Error updating testimonial:', error);
alert('Failed to update testimonial');
this.submitting = false;
}
});
} else {
this.testimonialService.createTestimonial(testimonialData).subscribe({
next: () => {
alert('Testimonial created successfully');
this.router.navigate(['/dashboard/testimonial/list']);
},
error: (error) => {
console.error('Error creating testimonial:', error);
alert('Failed to create testimonial');
this.submitting = false;
}
});
}
}
prepareFormData(): any {
const formValue = this.testimonialForm.value;
return {
name: formValue.name,
age: parseInt(formValue.age),
title: formValue.title,
story: formValue.story,
outcome: formValue.outcome,
impact: formValue.impact,
category: formValue.category,
isActive: formValue.isActive
};
}
onCancel(): void {
this.router.navigate(['/dashboard/testimonial/list']);
}
getCharacterCount(fieldName: string): number {
const control = this.testimonialForm.get(fieldName);
return control?.value?.length || 0;
}
getMaxLength(fieldName: string): number {
const maxLengths: { [key: string]: number } = {
'name': 100,
'title': 200
};
return maxLengths[fieldName] || 0;
}
}
@@ -0,0 +1,507 @@
/* Testimonial Layout */
.testimonial-layout {
display: flex;
min-height: 100vh;
background: #f8f9fa;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 260px;
z-index: 1000;
}
.testimonial-content {
margin-left: 260px;
flex: 1;
padding: 32px;
width: calc(100% - 260px);
max-width: 1600px;
}
/* Header Section */
.testimonial-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
gap: 20px;
}
.header-left {
flex: 1;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 4px 0;
letter-spacing: -0.5px;
}
.page-subtitle {
font-size: 14px;
color: #6b7280;
margin: 0;
}
.header-actions {
display: flex;
gap: 12px;
}
.btn-primary {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
/* Search Section */
.search-section {
margin-bottom: 24px;
}
.search-wrapper {
position: relative;
max-width: 600px;
}
.search-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
font-size: 16px;
}
.search-input {
width: 100%;
padding: 12px 48px 12px 48px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
color: #1a1a1a;
transition: all 0.2s ease;
background: white;
}
.search-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.search-input::placeholder {
color: #9ca3af;
}
.clear-search {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
border: none;
background: #f3f4f6;
border-radius: 50%;
color: #6b7280;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.clear-search:hover {
background: #e5e7eb;
color: #1a1a1a;
}
/* Loading State */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
}
.spinner {
font-size: 48px;
color: #3b82f6;
margin-bottom: 16px;
}
.loading-state p {
font-size: 14px;
color: #6b7280;
margin: 0;
}
/* Table Container */
.table-container {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.table-wrapper {
overflow-x: auto;
}
/* Testimonials Table */
.testimonials-table {
width: 100%;
border-collapse: collapse;
}
.testimonials-table thead {
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.testimonials-table thead th {
padding: 16px 20px;
text-align: left;
font-size: 13px;
font-weight: 600;
color: #374151;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
.testimonials-table tbody tr {
border-bottom: 1px solid #f3f4f6;
transition: background 0.2s ease;
}
.testimonials-table tbody tr:last-child {
border-bottom: none;
}
.testimonials-table tbody tr:hover {
background: #f9fafb;
}
.testimonials-table tbody td {
padding: 16px 20px;
font-size: 14px;
color: #374151;
vertical-align: middle;
}
/* Patient Cell */
.patient-cell {
display: flex;
align-items: center;
gap: 12px;
}
.patient-cell i {
font-size: 32px;
color: #3b82f6;
flex-shrink: 0;
}
.patient-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.patient-name {
font-weight: 600;
color: #1a1a1a;
font-size: 14px;
}
.patient-age {
font-size: 12px;
color: #6b7280;
}
/* Title Cell */
.title-cell {
font-weight: 500;
color: #1a1a1a;
max-width: 300px;
}
/* Category Badge */
.category-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: #eff6ff;
color: #1e40af;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.category-badge i {
font-size: 11px;
}
/* Story Preview */
.story-preview {
color: #6b7280;
font-size: 13px;
line-height: 1.5;
max-width: 350px;
}
/* Date Cell */
.date-cell {
display: flex;
align-items: center;
gap: 8px;
color: #6b7280;
font-size: 13px;
white-space: nowrap;
}
.date-cell i {
color: #9ca3af;
font-size: 14px;
}
/* Status Badge */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.status-active {
background: #d1fae5;
color: #065f46;
}
.status-active:hover {
background: #a7f3d0;
}
.status-inactive {
background: #fee2e2;
color: #991b1b;
}
.status-inactive:hover {
background: #fecaca;
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 8px;
}
.btn-action {
width: 36px;
height: 36px;
border-radius: 6px;
border: 1px solid #e5e7eb;
background: white;
color: #6b7280;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
}
.btn-action i {
font-size: 14px;
}
.btn-action:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.btn-edit:hover {
background: #eff6ff;
border-color: #3b82f6;
color: #3b82f6;
}
.btn-edit:hover i {
color: #3b82f6;
}
.btn-delete:hover {
background: #fef2f2;
border-color: #dc2626;
color: #dc2626;
}
.btn-delete:hover i {
color: #dc2626;
}
/* Empty State */
.empty-state {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 80px 20px;
text-align: center;
animation: fadeIn 0.3s ease;
}
.empty-icon {
width: 80px;
height: 80px;
margin: 0 auto 24px;
background: #f3f4f6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.empty-icon i {
font-size: 36px;
color: #9ca3af;
}
.empty-state h3 {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
.empty-state p {
font-size: 14px;
color: #6b7280;
margin: 0 0 24px 0;
}
/* Responsive Design */
@media (max-width: 992px) {
.testimonial-content {
margin-left: 0;
width: 100%;
padding: 24px;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.testimonial-header {
flex-direction: column;
align-items: flex-start;
}
.testimonials-table {
min-width: 1000px;
}
}
@media (max-width: 768px) {
.testimonial-content {
padding: 20px;
}
.page-title {
font-size: 24px;
}
.search-wrapper {
max-width: 100%;
}
}
@media (max-width: 576px) {
.testimonial-content {
padding: 16px;
}
.page-title {
font-size: 20px;
}
.testimonial-header {
gap: 16px;
}
.header-actions {
width: 100%;
}
.btn-primary {
width: 100%;
justify-content: center;
}
}
/* Print Styles */
@media print {
.sidebar,
.header-actions,
.search-section,
.action-buttons {
display: none;
}
.testimonial-content {
margin-left: 0;
width: 100%;
}
}
@@ -0,0 +1,141 @@
<div class="testimonial-layout">
<!-- Sidebar -->
<app-menu class="sidebar"></app-menu>
<!-- Main Content -->
<div class="testimonial-content">
<!-- Header Section -->
<div class="testimonial-header">
<div class="header-left">
<h1 class="page-title">Testimonials Management</h1>
<p class="page-subtitle">Manage patient stories and community impact</p>
</div>
<div class="header-actions">
<button class="btn-primary" (click)="createTestimonial()">
<i class="fas fa-plus"></i>
<span>Add New Testimonial</span>
</button>
</div>
</div>
<!-- Search Section -->
<div class="search-section">
<div class="search-wrapper">
<i class="fas fa-search search-icon"></i>
<input
type="text"
class="search-input"
placeholder="Search testimonials by name, title, or category..."
[(ngModel)]="searchTerm"
(input)="filterTestimonials()">
<button *ngIf="searchTerm" class="clear-search" (click)="searchTerm = ''; filterTestimonials()">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<!-- Loading State -->
<div *ngIf="loading" class="loading-state">
<div class="spinner">
<i class="fas fa-spinner fa-spin"></i>
</div>
<p>Loading testimonials...</p>
</div>
<!-- Testimonials Table -->
<div *ngIf="!loading && filteredTestimonials.length > 0" class="table-container">
<div class="table-wrapper">
<table class="testimonials-table">
<thead>
<tr>
<th>Patient</th>
<th>Title</th>
<th>Category</th>
<th>Story Preview</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let testimonial of filteredTestimonials">
<td>
<div class="patient-cell">
<i class="fas fa-user-circle"></i>
<div class="patient-info">
<span class="patient-name">{{ testimonial.name }}</span>
<span class="patient-age">{{ testimonial.age }} years old</span>
</div>
</div>
</td>
<td>
<div class="title-cell">
{{ truncateText(testimonial.title, 40) }}
</div>
</td>
<td>
<span class="category-badge">
<i class="fas fa-tag"></i>
{{ testimonial.category }}
</span>
</td>
<td>
<div class="story-preview">
{{ truncateText(testimonial.story, 60) }}
</div>
</td>
<td>
<span
class="status-badge"
[class.status-active]="testimonial.isActive"
[class.status-inactive]="!testimonial.isActive"
(click)="toggleActive(testimonial)"
title="Click to toggle status">
<i class="fas"
[class.fa-check-circle]="testimonial.isActive"
[class.fa-times-circle]="!testimonial.isActive"></i>
{{ testimonial.isActive ? 'Active' : 'Inactive' }}
</span>
</td>
<td>
<div class="date-cell">
<i class="fas fa-calendar"></i>
<span>{{ formatDate(testimonial.createdAt) }}</span>
</div>
</td>
<td>
<div class="action-buttons">
<button
class="btn-action btn-edit"
(click)="editTestimonial(testimonial.id!)"
title="Edit">
<i class="fas fa-edit"></i>
</button>
<button
class="btn-action btn-delete"
(click)="deleteTestimonial(testimonial.id!, testimonial.name)"
title="Delete">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Empty State -->
<div *ngIf="!loading && filteredTestimonials.length === 0" class="empty-state">
<div class="empty-icon">
<i class="fas fa-comments"></i>
</div>
<h3>{{ searchTerm ? 'No testimonials found' : 'No testimonials yet' }}</h3>
<p>{{ searchTerm ? 'Try adjusting your search criteria' : 'Get started by adding your first patient testimonial' }}</p>
<button *ngIf="!searchTerm" class="btn-primary" (click)="createTestimonial()">
<i class="fas fa-plus"></i>
<span>Add First Testimonial</span>
</button>
</div>
</div>
</div>
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TestimonialListComponent } from './testimonial-list.component';
describe('TestimonialListComponent', () => {
let component: TestimonialListComponent;
let fixture: ComponentFixture<TestimonialListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ TestimonialListComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TestimonialListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,116 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { TestimonialService } from '../../../service/testimonial.service';
import { Testimonial } from '../../../model/testimonial.model';
@Component({
selector: 'app-testimonial-list',
templateUrl: './testimonial-list.component.html',
styleUrls: ['./testimonial-list.component.css']
})
export class TestimonialListComponent implements OnInit {
testimonials: Testimonial[] = [];
loading: boolean = false;
searchTerm: string = '';
filteredTestimonials: Testimonial[] = [];
constructor(
private testimonialService: TestimonialService,
private router: Router
) {}
ngOnInit(): void {
this.loadTestimonials();
}
loadTestimonials(): void {
this.loading = true;
this.testimonialService.getAllTestimonials().subscribe({
next: (data) => {
this.testimonials = data;
this.filteredTestimonials = data;
this.loading = false;
},
error: (error) => {
console.error('Error loading testimonials:', error);
alert('Failed to load testimonials');
this.loading = false;
}
});
}
filterTestimonials(): void {
if (!this.searchTerm) {
this.filteredTestimonials = this.testimonials;
return;
}
const term = this.searchTerm.toLowerCase();
this.filteredTestimonials = this.testimonials.filter(testimonial =>
testimonial.name.toLowerCase().includes(term) ||
testimonial.title.toLowerCase().includes(term) ||
testimonial.category.toLowerCase().includes(term)
);
}
createTestimonial(): void {
this.router.navigate(['/dashboard/testimonial/create']);
}
editTestimonial(id: number): void {
this.router.navigate(['/dashboard/testimonial/edit', id]);
}
deleteTestimonial(id: number, name: string): void {
if (confirm(`Are you sure you want to delete testimonial of "${name}"?`)) {
this.testimonialService.deleteTestimonial(id).subscribe({
next: () => {
alert('Testimonial deleted successfully');
this.loadTestimonials();
},
error: (error) => {
console.error('Error deleting testimonial:', error);
alert('Failed to delete testimonial');
}
});
}
}
toggleActive(testimonial: Testimonial): void {
const updatedTestimonial: any = {
name: testimonial.name,
age: testimonial.age,
title: testimonial.title,
story: testimonial.story,
outcome: testimonial.outcome,
impact: testimonial.impact,
category: testimonial.category,
isActive: !testimonial.isActive
};
this.testimonialService.updateTestimonial(testimonial.id!, updatedTestimonial).subscribe({
next: () => {
alert('Testimonial status updated');
this.loadTestimonials();
},
error: (error) => {
console.error('Error updating testimonial:', error);
alert('Failed to update testimonial status');
}
});
}
formatDate(date: Date | string | undefined): string {
if (!date) return 'N/A';
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
}
}