feat: implement product carousel component with thumbnail gallery and navigation controls

This commit is contained in:
2026-06-30 10:11:16 -03:00
parent cc16a53fac
commit ec261caab3
2 changed files with 38 additions and 49 deletions

View File

@@ -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>

View File

@@ -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;