Hero and Service tile added newly
This commit is contained in:
@@ -0,0 +1,638 @@
|
||||
/* Hero Image Layout */
|
||||
.hero-image-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
width: 260px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.hero-image-content {
|
||||
margin-left: 260px;
|
||||
flex: 1;
|
||||
padding: 32px;
|
||||
width: calc(100% - 260px);
|
||||
max-width: 1600px;
|
||||
}
|
||||
|
||||
/* Header Section */
|
||||
.hero-image-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;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Search Box */
|
||||
.search-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-box i {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 10px 16px 10px 42px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #1a1a1a;
|
||||
width: 300px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary,
|
||||
.btn-secondary,
|
||||
.btn-refresh {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
color: #374151;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #f9fafb;
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
.btn-refresh {
|
||||
background: white;
|
||||
color: #6b7280;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.btn-refresh:hover {
|
||||
background: #f9fafb;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Hero Images Grid */
|
||||
.hero-images-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.hero-image-card {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.hero-image-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.hero-image-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
background: #f3f4f6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero-image-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.active-badge {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.hero-image-info {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.hero-image-info h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.hero-image-info .subtitle {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hero-image-info .description {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-image-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
flex: 1;
|
||||
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;
|
||||
}
|
||||
|
||||
.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-activate:hover {
|
||||
background: #fef3c7;
|
||||
border-color: #f59e0b;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: #fef2f2;
|
||||
border-color: #dc2626;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.loading-state {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 20px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-spinner i {
|
||||
font-size: 36px;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.loading-state p {
|
||||
font-size: 16px;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
background: white;
|
||||
border: 2px dashed #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 80px 20px;
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: 0 auto 24px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.empty-icon i {
|
||||
font-size: 48px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 16px;
|
||||
color: #6b7280;
|
||||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.empty-state .btn-primary {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24px 32px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: #e5e7eb;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px 32px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 20px 32px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* Hero Image Detail */
|
||||
.hero-image-detail-card {
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-image-preview-large {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-image-preview-large img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.hero-image-details {
|
||||
padding: 24px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #1a1a1a;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.form-section {
|
||||
margin: 24px 0;
|
||||
padding: 20px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.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-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/* File Upload */
|
||||
.file-upload-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.file-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border: 2px dashed #d1d5db;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.file-label:hover {
|
||||
border-color: #3b82f6;
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
.file-label i {
|
||||
font-size: 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.file-label span {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.current-image-preview {
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.current-image-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 992px) {
|
||||
.hero-image-content {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.hero-image-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hero-images-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-image-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.hero-images-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-body,
|
||||
.modal-footer {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.hero-image-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-actions > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
<div class="hero-image-layout">
|
||||
<!-- Sidebar -->
|
||||
<app-menu class="sidebar"></app-menu>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="hero-image-content">
|
||||
<!-- Header Section -->
|
||||
<div class="hero-image-header">
|
||||
<div class="header-left">
|
||||
<h1 class="page-title">Hero Image Management</h1>
|
||||
<p class="page-subtitle">Manage homepage hero section images</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="search-box">
|
||||
<i class="fa fa-search"></i>
|
||||
<input
|
||||
name="searchTerm"
|
||||
#searchTerm="ngModel"
|
||||
class="search-input"
|
||||
type="search"
|
||||
placeholder="Search hero images..."
|
||||
ngModel
|
||||
(ngModelChange)="searchHeroImages(searchTerm.value)">
|
||||
</div>
|
||||
<button *ngIf="isManager" class="btn-primary" data-bs-toggle="modal" data-bs-target="#addHeroImageModal">
|
||||
<i class="fa fa-plus"></i>
|
||||
<span>New Hero Image</span>
|
||||
</button>
|
||||
<button class="btn-refresh" (click)="getHeroImages(true)">
|
||||
<i class="fas fa-sync" [ngClass]="{'fa-spin': refreshing }"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero Images Grid -->
|
||||
<div class="hero-images-grid" *ngIf="heroImages && heroImages.length > 0">
|
||||
<div *ngFor="let heroImage of heroImages" class="hero-image-card" (click)="heroImage && onSelectHeroImage(heroImage)">
|
||||
<div class="hero-image-preview" *ngIf="heroImage">
|
||||
<img [src]="heroImage?.imageUrl"
|
||||
[alt]="heroImage?.title || 'Hero Image'"
|
||||
(error)="onImageError($event)"
|
||||
loading="lazy">
|
||||
<div class="active-badge" *ngIf="heroImage?.isActive">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
<span>Active</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-image-info" *ngIf="heroImage">
|
||||
<h3>{{ heroImage?.title || 'Untitled' }}</h3>
|
||||
<p class="subtitle" *ngIf="heroImage?.subtitle">{{ heroImage.subtitle }}</p>
|
||||
<p class="description" *ngIf="heroImage?.description">{{ heroImage.description }}</p>
|
||||
<div class="hero-image-actions">
|
||||
<button class="btn-action btn-edit" (click)="onEditHeroImage(heroImage); $event.stopPropagation()" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn-action btn-activate"
|
||||
*ngIf="!heroImage?.isActive"
|
||||
(click)="onSetActiveHeroImage(heroImage); $event.stopPropagation()"
|
||||
title="Set as Active">
|
||||
<i class="fas fa-star"></i>
|
||||
</button>
|
||||
<button *ngIf="isAdmin && !heroImage?.isActive"
|
||||
class="btn-action btn-delete"
|
||||
(click)="onDeleteHeroImage(heroImage); $event.stopPropagation()"
|
||||
title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div *ngIf="refreshing" class="loading-state">
|
||||
<div class="loading-spinner">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
<p>Loading hero images...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div *ngIf="!refreshing && (!heroImages || heroImages.length === 0)" class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<i class="fa fa-image"></i>
|
||||
</div>
|
||||
<h3>No hero images yet</h3>
|
||||
<p>Get started by creating your first hero image for the homepage</p>
|
||||
<button *ngIf="isManager" class="btn-primary" data-bs-toggle="modal" data-bs-target="#addHeroImageModal">
|
||||
<i class="fa fa-plus"></i>
|
||||
<span>Create Your First Hero Image</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Modal Triggers -->
|
||||
<button [hidden]="true" type="button" id="openHeroImageInfo" data-bs-toggle="modal" data-bs-target="#viewHeroImageModal"></button>
|
||||
<button [hidden]="true" type="button" id="openHeroImageEdit" data-bs-toggle="modal" data-bs-target="#editHeroImageModal"></button>
|
||||
|
||||
<!-- View Hero Image Modal -->
|
||||
<div class="modal fade" id="viewHeroImageModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content" *ngIf="selectedHeroImage">
|
||||
<div class="modal-header">
|
||||
<h3>Hero Image Details</h3>
|
||||
<button class="modal-close" data-bs-dismiss="modal">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="hero-image-detail-card">
|
||||
<div class="hero-image-preview-large">
|
||||
<img [src]="selectedHeroImage.imageUrl" [alt]="selectedHeroImage.title">
|
||||
<div class="active-badge" *ngIf="selectedHeroImage.isActive">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
<span>Active</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-image-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Title:</span>
|
||||
<span class="detail-value">{{ selectedHeroImage.title }}</span>
|
||||
</div>
|
||||
<div class="detail-item" *ngIf="selectedHeroImage.subtitle">
|
||||
<span class="detail-label">Subtitle:</span>
|
||||
<span class="detail-value">{{ selectedHeroImage.subtitle }}</span>
|
||||
</div>
|
||||
<div class="detail-item" *ngIf="selectedHeroImage.description">
|
||||
<span class="detail-label">Description:</span>
|
||||
<span class="detail-value">{{ selectedHeroImage.description }}</span>
|
||||
</div>
|
||||
<div class="detail-item" *ngIf="selectedHeroImage.uploadDate">
|
||||
<span class="detail-label">Uploaded:</span>
|
||||
<span class="detail-value">{{ selectedHeroImage.uploadDate | date:'MMM d, y h:mm a' }}</span>
|
||||
</div>
|
||||
<div class="detail-item" *ngIf="selectedHeroImage.lastModified">
|
||||
<span class="detail-label">Last Modified:</span>
|
||||
<span class="detail-value">{{ selectedHeroImage.lastModified | date:'MMM d, y h:mm a' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Hero Image Modal -->
|
||||
<div *ngIf="isManager" class="modal fade" id="addHeroImageModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Add New Hero Image</h3>
|
||||
<button class="modal-close" data-bs-dismiss="modal">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form #newHeroImageForm="ngForm" (ngSubmit)="onAddNewHeroImage(newHeroImageForm)">
|
||||
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Image Information</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="title">
|
||||
<i class="fa fa-heading"></i>
|
||||
Title *
|
||||
</label>
|
||||
<input type="text" id="title" name="title" class="form-input" ngModel required placeholder="Enter title">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="subtitle">
|
||||
<i class="fa fa-align-left"></i>
|
||||
Subtitle
|
||||
</label>
|
||||
<input type="text" id="subtitle" name="subtitle" class="form-input" ngModel placeholder="Enter subtitle">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">
|
||||
<i class="fa fa-align-left"></i>
|
||||
Description
|
||||
</label>
|
||||
<textarea id="description" name="description" class="form-textarea" rows="3" ngModel placeholder="Enter description"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<i class="fa fa-image"></i>
|
||||
Upload Image *
|
||||
</label>
|
||||
<div class="file-upload-wrapper">
|
||||
<input type="file"
|
||||
id="newHeroImage"
|
||||
accept="image/*"
|
||||
name="heroImage"
|
||||
(change)="onImageChange($any($event).target.files)"
|
||||
class="file-input"
|
||||
required>
|
||||
<label for="newHeroImage" class="file-label">
|
||||
<i class="fa fa-cloud-upload-alt"></i>
|
||||
<span>{{ heroImageFileName || 'Choose image (required)' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text text-muted" *ngIf="!heroImageFile">
|
||||
Please upload an image file (JPG, PNG, or GIF)
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button class="btn-primary"
|
||||
type="button"
|
||||
(click)="newHeroImageForm.ngSubmit.emit()"
|
||||
[disabled]="newHeroImageForm.invalid || !heroImageFile">
|
||||
<i class="fa fa-save"></i>
|
||||
Create Hero Image
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Hero Image Modal -->
|
||||
<div class="modal fade" id="editHeroImageModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content" *ngIf="selectedHeroImage">
|
||||
<div class="modal-header">
|
||||
<h3>Edit Hero Image</h3>
|
||||
<button class="modal-close" data-bs-dismiss="modal">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form #editHeroImageForm="ngForm" (ngSubmit)="onUpdateHeroImage(editHeroImageForm)">
|
||||
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Image Information</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editTitle">
|
||||
<i class="fa fa-heading"></i>
|
||||
Title *
|
||||
</label>
|
||||
<input type="text" id="editTitle" name="title" class="form-input" [(ngModel)]="selectedHeroImage.title" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editSubtitle">
|
||||
<i class="fa fa-align-left"></i>
|
||||
Subtitle
|
||||
</label>
|
||||
<input type="text" id="editSubtitle" name="subtitle" class="form-input" [(ngModel)]="selectedHeroImage.subtitle">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editDescription">
|
||||
<i class="fa fa-align-left"></i>
|
||||
Description
|
||||
</label>
|
||||
<textarea id="editDescription" name="description" class="form-textarea" rows="3" [(ngModel)]="selectedHeroImage.description"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<i class="fa fa-image"></i>
|
||||
Current Image
|
||||
</label>
|
||||
<div class="current-image-preview">
|
||||
<img [src]="selectedHeroImage.imageUrl" [alt]="selectedHeroImage.title">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<i class="fa fa-image"></i>
|
||||
Upload New Image (Optional)
|
||||
</label>
|
||||
<div class="file-upload-wrapper">
|
||||
<input type="file"
|
||||
id="editHeroImage"
|
||||
accept="image/*"
|
||||
name="heroImage"
|
||||
(change)="onImageChange($any($event).target.files)"
|
||||
class="file-input">
|
||||
<label for="editHeroImage" class="file-label">
|
||||
<i class="fa fa-cloud-upload-alt"></i>
|
||||
<span>{{ heroImageFileName || 'Choose new image' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button class="btn-primary" (click)="editHeroImageForm.ngSubmit.emit()" [disabled]="editHeroImageForm.invalid">
|
||||
<i class="fa fa-save"></i>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HeroImageComponent } from './hero-image.component';
|
||||
|
||||
describe('HeroImageComponent', () => {
|
||||
let component: HeroImageComponent;
|
||||
let fixture: ComponentFixture<HeroImageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ HeroImageComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HeroImageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,269 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { HeroImage } from '../../model/hero-image.model';
|
||||
import { NotificationService } from '../../service/notification.service';
|
||||
import { NotificationType } from '../../notification/notification-type';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { NgForm } from '@angular/forms';
|
||||
import { CustomHttpResponse } from '../../dto/custom-http-response';
|
||||
import { SubSink } from 'subsink';
|
||||
import { User } from 'src/app/model/user';
|
||||
import { Role } from 'src/app/enum/role.enum';
|
||||
import { AuthenticationService } from 'src/app/service/authentication.service';
|
||||
import { HeroImageService } from 'src/app/service/hero-image.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-image',
|
||||
templateUrl: './hero-image.component.html',
|
||||
styleUrls: ['./hero-image.component.css']
|
||||
})
|
||||
export class HeroImageComponent implements OnInit, OnDestroy {
|
||||
private titleSubject = new BehaviorSubject<string>('Hero Images');
|
||||
public titleAction$ = this.titleSubject.asObservable();
|
||||
public loggedInUser: User;
|
||||
|
||||
public heroImages: HeroImage[] = [];
|
||||
public selectedHeroImage: HeroImage | null = null;
|
||||
public refreshing: boolean = false;
|
||||
private subs = new SubSink();
|
||||
|
||||
public heroImageFile: File | null = null;
|
||||
public heroImageFileName: string | null = null;
|
||||
|
||||
constructor(
|
||||
private heroImageService: HeroImageService,
|
||||
private notificationService: NotificationService,
|
||||
private authenticationService: AuthenticationService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getHeroImages(false); // Don't show notification on initial load
|
||||
this.loggedInUser = this.authenticationService.getUserFromLocalStorage();
|
||||
this.setupModalEventListeners();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.unsubscribe();
|
||||
}
|
||||
|
||||
private setupModalEventListeners(): void {
|
||||
const editModal = document.getElementById('editHeroImageModal');
|
||||
if (editModal) {
|
||||
editModal.addEventListener('hidden.bs.modal', () => {
|
||||
this.clearEditHeroImageData();
|
||||
});
|
||||
}
|
||||
|
||||
const addModal = document.getElementById('addHeroImageModal');
|
||||
if (addModal) {
|
||||
addModal.addEventListener('hidden.bs.modal', () => {
|
||||
this.clearNewHeroImageData();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private clearNewHeroImageData(): void {
|
||||
this.heroImageFile = null;
|
||||
this.heroImageFileName = null;
|
||||
}
|
||||
|
||||
private clearEditHeroImageData(): void {
|
||||
this.heroImageFile = null;
|
||||
this.heroImageFileName = null;
|
||||
this.selectedHeroImage = null;
|
||||
}
|
||||
|
||||
private invalidateVariables(): void {
|
||||
this.heroImageFile = null;
|
||||
this.heroImageFileName = null;
|
||||
}
|
||||
|
||||
public getHeroImages(showNotification: boolean): void {
|
||||
this.refreshing = true;
|
||||
this.subs.sink = this.heroImageService.getAllHeroImages().subscribe(
|
||||
heroImages => {
|
||||
console.log('Raw hero images response:', heroImages);
|
||||
|
||||
// Filter out any null or undefined items
|
||||
this.heroImages = (heroImages || []).filter(img => img !== null && img !== undefined);
|
||||
|
||||
console.log('Filtered hero images:', this.heroImages);
|
||||
console.log('Hero images count:', this.heroImages.length);
|
||||
|
||||
this.heroImageService.addHeroImagesToLocalStorage(this.heroImages);
|
||||
|
||||
// Only show notification if explicitly requested (e.g., manual refresh)
|
||||
if (showNotification && this.heroImages.length > 0) {
|
||||
this.notificationService.notify(NotificationType.SUCCESS, `Loaded ${this.heroImages.length} hero image${this.heroImages.length > 1 ? 's' : ''}`);
|
||||
}
|
||||
this.refreshing = false;
|
||||
},
|
||||
(errorResponse: HttpErrorResponse) => {
|
||||
console.error('Error loading hero images:', errorResponse);
|
||||
this.sendErrorNotification(errorResponse.error?.message || 'Failed to load hero images');
|
||||
this.refreshing = false;
|
||||
this.heroImages = []; // Set to empty array on error
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public onSelectHeroImage(heroImage: HeroImage): void {
|
||||
this.selectedHeroImage = JSON.parse(JSON.stringify(heroImage));
|
||||
this.clickButton('openHeroImageInfo');
|
||||
}
|
||||
|
||||
public onImageChange(fileList: FileList): void {
|
||||
if (fileList && fileList.length > 0) {
|
||||
this.heroImageFileName = fileList[0].name;
|
||||
this.heroImageFile = fileList[0];
|
||||
}
|
||||
}
|
||||
|
||||
private sendErrorNotification(message: string): void {
|
||||
this.sendNotification(NotificationType.ERROR, message);
|
||||
}
|
||||
|
||||
private sendNotification(type: NotificationType, message: string): void {
|
||||
this.notificationService.notify(type, message ? message : 'An error occurred. Please try again');
|
||||
}
|
||||
|
||||
public onAddNewHeroImage(heroImageForm: NgForm): void {
|
||||
const formData = this.createHeroImageFormData(heroImageForm.value, this.heroImageFile);
|
||||
this.subs.sink = this.heroImageService.addHeroImage(formData).subscribe(
|
||||
(heroImage: HeroImage) => {
|
||||
this.getHeroImages(false);
|
||||
heroImageForm.reset();
|
||||
this.clearNewHeroImageData();
|
||||
// Close modal using Bootstrap modal API
|
||||
const modalElement = document.getElementById('addHeroImageModal');
|
||||
if (modalElement) {
|
||||
const modal = (window as any).bootstrap.Modal.getInstance(modalElement);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
(errorResponse: HttpErrorResponse) => {
|
||||
this.sendErrorNotification(errorResponse.error.message);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public searchHeroImages(searchTerm: string): void {
|
||||
if (!searchTerm) {
|
||||
this.heroImages = this.heroImageService.getHeroImagesFromLocalStorage();
|
||||
return;
|
||||
}
|
||||
|
||||
const matchHeroImages: HeroImage[] = [];
|
||||
searchTerm = searchTerm.toLowerCase();
|
||||
for (const heroImage of this.heroImageService.getHeroImagesFromLocalStorage()) {
|
||||
if (
|
||||
(heroImage.title && heroImage.title.toLowerCase().includes(searchTerm)) ||
|
||||
(heroImage.subtitle && heroImage.subtitle.toLowerCase().includes(searchTerm)) ||
|
||||
(heroImage.description && heroImage.description.toLowerCase().includes(searchTerm))
|
||||
) {
|
||||
matchHeroImages.push(heroImage);
|
||||
}
|
||||
}
|
||||
this.heroImages = matchHeroImages;
|
||||
}
|
||||
|
||||
private clickButton(buttonId: string): void {
|
||||
document.getElementById(buttonId)?.click();
|
||||
}
|
||||
|
||||
public onEditHeroImage(heroImage: HeroImage): void {
|
||||
this.selectedHeroImage = JSON.parse(JSON.stringify(heroImage));
|
||||
this.clickButton('openHeroImageEdit');
|
||||
}
|
||||
|
||||
public onUpdateHeroImage(form: NgForm): void {
|
||||
if (form.invalid || !this.selectedHeroImage) {
|
||||
this.notificationService.notify(NotificationType.ERROR, 'Please fill out all required fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = this.createHeroImageFormData(this.selectedHeroImage, this.heroImageFile);
|
||||
|
||||
this.subs.add(this.heroImageService.updateHeroImage(this.selectedHeroImage.id!, formData).subscribe(
|
||||
(heroImage: HeroImage) => {
|
||||
this.getHeroImages(false);
|
||||
this.invalidateVariables();
|
||||
// Close modal using Bootstrap modal API
|
||||
const modalElement = document.getElementById('editHeroImageModal');
|
||||
if (modalElement) {
|
||||
const modal = (window as any).bootstrap.Modal.getInstance(modalElement);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
(errorResponse: HttpErrorResponse) => {
|
||||
this.sendErrorNotification(errorResponse.error.message);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
private createHeroImageFormData(heroImage: any, imageFile: File | null): FormData {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('title', heroImage.title || '');
|
||||
formData.append('subtitle', heroImage.subtitle || '');
|
||||
formData.append('description', heroImage.description || '');
|
||||
|
||||
if (imageFile) {
|
||||
formData.append('image', imageFile);
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
public onDeleteHeroImage(heroImage: HeroImage): void {
|
||||
if (heroImage.isActive) {
|
||||
this.notificationService.notify(NotificationType.ERROR, 'Cannot delete active hero image. Please set another image as active first.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm(`Are you sure you want to delete this hero image?`)) {
|
||||
this.subs.sink = this.heroImageService.deleteHeroImage(heroImage.id!).subscribe(
|
||||
(response: CustomHttpResponse) => {
|
||||
this.getHeroImages(false);
|
||||
this.invalidateVariables();
|
||||
this.notificationService.notify(NotificationType.SUCCESS, response.message);
|
||||
},
|
||||
(errorResponse: HttpErrorResponse) => {
|
||||
this.sendErrorNotification(errorResponse.error.message);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public onSetActiveHeroImage(heroImage: HeroImage): void {
|
||||
if (confirm(`Set this hero image as active?`)) {
|
||||
this.subs.sink = this.heroImageService.setActiveHeroImage(heroImage.id!).subscribe(
|
||||
(response: HeroImage) => {
|
||||
this.getHeroImages(false);
|
||||
// Silently refresh without notification
|
||||
},
|
||||
(errorResponse: HttpErrorResponse) => {
|
||||
this.sendErrorNotification(errorResponse.error.message);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public get isAdmin(): boolean {
|
||||
return this.loggedInUser && (this.loggedInUser.role === Role.ADMIN || this.loggedInUser.role === Role.SUPER_ADMIN);
|
||||
}
|
||||
|
||||
public get isManager(): boolean {
|
||||
return this.isAdmin || (this.loggedInUser && this.loggedInUser.role === Role.MANAGER);
|
||||
}
|
||||
|
||||
public onImageError(event: any): void {
|
||||
// Set a placeholder image when image fails to load
|
||||
event.target.src = 'assets/images/placeholder.jpg';
|
||||
console.error('Failed to load hero image');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user