Further updates on 03-11-2025
This commit is contained in:
+535
@@ -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%;
|
||||
}
|
||||
}
|
||||
+257
@@ -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>
|
||||
+25
@@ -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();
|
||||
});
|
||||
});
|
||||
+157
@@ -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;
|
||||
}
|
||||
}
|
||||
+507
@@ -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%;
|
||||
}
|
||||
}
|
||||
+141
@@ -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>
|
||||
+25
@@ -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();
|
||||
});
|
||||
});
|
||||
+116
@@ -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) + '...';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user