feat: implement product carousel component with thumbnail gallery and navigation controls
This commit is contained in:
@@ -1,63 +1,51 @@
|
|||||||
<div class="product-carousel d-flex flex-column gap-3">
|
<div class="product-carousel d-flex flex-column gap-3">
|
||||||
<!-- Active/Main Image Area -->
|
<!-- Active/Main Image Area -->
|
||||||
<div class="product-carousel__main position-relative bg-light rounded overflow-hidden">
|
<div class="product-carousel__main position-relative bg-light overflow-hidden">
|
||||||
<!-- Main Image -->
|
<!-- Main Image -->
|
||||||
@if (images().length > 0) {
|
@if (images().length > 0) {
|
||||||
<img
|
<img [src]="images()[activeIndex()]" alt="Product active image"
|
||||||
[src]="images()[activeIndex()]"
|
class="product-carousel__main-image w-100 h-100 object-fit-cover" />
|
||||||
alt="Product active image"
|
|
||||||
class="product-carousel__main-image w-100 h-100 object-fit-cover"
|
|
||||||
/>
|
|
||||||
} @else {
|
} @else {
|
||||||
<!-- Placeholder -->
|
<!-- Placeholder -->
|
||||||
<div class="product-carousel__placeholder w-100 h-100 d-flex align-items-center justify-content-center text-muted">
|
<div class="product-carousel__placeholder w-100 h-100 d-flex align-items-center justify-content-center text-muted">
|
||||||
<i class="fa-solid fa-image fa-3x"></i>
|
<i class="fa-solid fa-image fa-3x"></i>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Discount Badge -->
|
<!-- Discount Badge -->
|
||||||
@if (discount() && discount()! > 0) {
|
@if (discount() && discount()! > 0) {
|
||||||
<span class="product-carousel__discount-badge position-absolute top-0 end-0 bg-primary text-white px-3 py-2 fw-semibold">
|
<span
|
||||||
-{{ discount() }}%
|
class="product-carousel__discount-badge position-absolute top-0 end-0 bg-primary text-white px-3 py-2 fw-semibold">
|
||||||
</span>
|
-{{ discount() }}%
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Navigation Arrows -->
|
<!-- Navigation Arrows -->
|
||||||
@if (images().length > 1) {
|
@if (images().length > 1) {
|
||||||
<button
|
<button type="button"
|
||||||
type="button"
|
class="product-carousel__nav-btn product-carousel__nav-btn--prev position-absolute top-50 translate-middle-y border-0 p-0 d-flex align-items-center justify-content-center"
|
||||||
class="product-carousel__nav-btn product-carousel__nav-btn--prev position-absolute top-50 translate-middle-y border-0 p-0 d-flex align-items-center justify-content-center"
|
(click)="prevImage()" aria-label="Previous image">
|
||||||
(click)="prevImage()"
|
<i class="fa-solid fa-chevron-left"></i>
|
||||||
aria-label="Previous image"
|
</button>
|
||||||
>
|
|
||||||
<i class="fa-solid fa-chevron-left"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button type="button"
|
||||||
type="button"
|
class="product-carousel__nav-btn product-carousel__nav-btn--next position-absolute top-50 translate-middle-y border-0 p-0 d-flex align-items-center justify-content-center"
|
||||||
class="product-carousel__nav-btn product-carousel__nav-btn--next position-absolute top-50 translate-middle-y border-0 p-0 d-flex align-items-center justify-content-center"
|
(click)="nextImage()" aria-label="Next image">
|
||||||
(click)="nextImage()"
|
<i class="fa-solid fa-chevron-right"></i>
|
||||||
aria-label="Next image"
|
</button>
|
||||||
>
|
|
||||||
<i class="fa-solid fa-chevron-right"></i>
|
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Thumbnails Row -->
|
<!-- Thumbnails Row -->
|
||||||
@if (images().length > 1) {
|
@if (images().length > 1) {
|
||||||
<div class="product-carousel__thumbnails d-flex gap-3">
|
<div class="product-carousel__thumbnails d-flex gap-3">
|
||||||
@for (image of images(); track image; let idx = $index) {
|
@for (image of images(); track image; let idx = $index) {
|
||||||
<button
|
<button type="button" class="product-carousel__thumbnail border-0 p-0 overflow-hidden bg-light"
|
||||||
type="button"
|
[class.product-carousel__thumbnail--active]="idx === activeIndex()" (click)="selectImage(idx)"
|
||||||
class="product-carousel__thumbnail border-0 p-0 rounded overflow-hidden bg-light"
|
[attr.aria-label]="'Select image ' + (idx + 1)">
|
||||||
[class.product-carousel__thumbnail--active]="idx === activeIndex()"
|
<img [src]="image" alt="Product thumbnail" class="w-100 h-100 object-fit-cover" />
|
||||||
(click)="selectImage(idx)"
|
</button>
|
||||||
[attr.aria-label]="'Select image ' + (idx + 1)"
|
}
|
||||||
>
|
</div>
|
||||||
<img [src]="image" alt="Product thumbnail" class="w-100 h-100 object-fit-cover" />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -2,10 +2,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&__main {
|
&__main {
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 463 / 489;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #e0e0e0 !important; /* Matches grey placeholder in mock */
|
background-color: #e0e0e0 !important;
|
||||||
border-radius: 8px;
|
/* Matches grey placeholder in mock */
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -63,8 +64,8 @@
|
|||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
background-color: #e0e0e0 !important; /* Matches mock layout */
|
background-color: #e0e0e0 !important;
|
||||||
border-radius: 4px;
|
/* Matches mock layout */
|
||||||
padding: 0;
|
padding: 0;
|
||||||
transition: opacity 0.2s ease, border-color 0.2s ease;
|
transition: opacity 0.2s ease, border-color 0.2s ease;
|
||||||
border: 3px solid transparent !important;
|
border: 3px solid transparent !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user