bug-fixes

This commit is contained in:
sdarbinyan
2026-07-09 01:40:22 +04:00
parent 92e1bdaff8
commit 55216817b2
59 changed files with 885 additions and 158 deletions

View File

@@ -1,4 +1,4 @@
<section class="product-gallery" aria-label="Product media">
<section class="product-gallery" [attr.aria-label]="'productDetails.mediaAria' | translate">
<div class="product-gallery-main">
@if (media[selectedIndex]?.video) {
<video [src]="media[selectedIndex].url" controls></video>

View File

@@ -13,6 +13,7 @@
display: flex;
align-items: center;
justify-content: center;
transition: box-shadow 0.24s ease, border-color 0.24s ease;
img,
video {
@@ -36,12 +37,18 @@
overflow: hidden;
background: #fff;
cursor: pointer;
transition: transform 0.2s ease, border-color 0.2s ease;
&.active {
border-color: #497671;
box-shadow: 0 0 0 3px rgba(73, 118, 113, 0.18);
}
&:hover {
transform: translateY(-1px);
border-color: #497671;
}
img {
width: 100%;
height: 100%;
@@ -59,3 +66,10 @@
color: #fff;
z-index: 1;
}
@media (max-width: 640px) {
.product-gallery-thumbs {
grid-template-columns: repeat(auto-fill, minmax(58px, 1fr));
gap: 8px;
}
}

View File

@@ -1,10 +1,12 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { Product } from '../../../../../core/products/models/product-domain.model';
import { getMainImage } from '../../../../../utils/item.utils';
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
@Component({
selector: 'app-product-gallery',
standalone: true,
imports: [TranslatePipe],
templateUrl: './product-gallery.component.html',
styleUrls: ['./product-gallery.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -3,7 +3,7 @@
@if (product.badges?.length) {
<div class="product-badges">
@for (badge of product.badges; track badge) {
@for (badge of product.badges; track $index) {
<span class="product-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
}
</div>

View File

@@ -99,8 +99,25 @@
font-size: 1rem;
font-weight: 900;
cursor: pointer;
transition: transform 0.2s ease, background-color 0.2s ease;
&:hover {
background: #3d635f;
transform: translateY(-1px);
}
&:focus-visible {
outline: 3px solid rgba(73, 118, 113, 0.25);
outline-offset: 2px;
}
}
@media (max-width: 640px) {
.product-information {
gap: 14px;
}
.product-price strong {
font-size: 1.7rem;
}
}

View File

@@ -1,5 +1,5 @@
<section class="product-specifications card section">
<h3>Specifications</h3>
<h3>{{ 'itemDetail.specifications' | translate }}</h3>
@if (descriptionFields.length > 0) {
<dl>
@@ -11,6 +11,6 @@
}
</dl>
} @else {
<p>No specifications provided yet.</p>
<p>{{ 'productDetails.specificationsEmpty' | translate }}</p>
}
</section>

View File

@@ -1,9 +1,11 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Product } from '../../../../../core/products/models/product-domain.model';
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
@Component({
selector: 'app-product-specifications',
standalone: true,
imports: [TranslatePipe],
templateUrl: './product-specifications.component.html',
styleUrls: ['./product-specifications.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -1,8 +1,8 @@
<section class="product-warranty card section">
<h3>Warranty and returns</h3>
<h3>{{ 'productDetails.warrantyTitle' | translate }}</h3>
<ul>
<li>Warranty terms are provided by the seller and local law.</li>
<li>Return eligibility depends on item condition and category.</li>
<li>Detailed return instructions are available after purchase.</li>
<li>{{ 'productDetails.warrantyItem1' | translate }}</li>
<li>{{ 'productDetails.warrantyItem2' | translate }}</li>
<li>{{ 'productDetails.warrantyItem3' | translate }}</li>
</ul>
</section>

View File

@@ -1,8 +1,10 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
@Component({
selector: 'app-product-warranty',
standalone: true,
imports: [TranslatePipe],
templateUrl: './product-warranty.component.html',
styleUrls: ['./product-warranty.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -4,7 +4,7 @@
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.colour' | translate }}</span>
<div class="variant-options">
@for (colour of colours; track colour) {
@for (colour of colours; track $index) {
<button type="button" class="colour-swatch" [class.active]="selectedColour === colour" [style.background-color]="colour" [attr.aria-label]="colour" (click)="colourSelected.emit(colour)"></button>
}
</div>
@@ -20,7 +20,7 @@
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.size' | translate }}</span>
<div class="variant-options">
@for (size of sizes; track size) {
@for (size of sizes; track $index) {
<button type="button" class="size-chip" [class.active]="selectedSize === size" (click)="sizeSelected.emit(size)">{{ size }}</button>
}
</div>

View File

@@ -4,13 +4,13 @@
padding: 24px;
display: flex;
flex-direction: column;
gap: 42px;
gap: 32px;
}
.product-details-layout {
display: grid;
grid-template-columns: minmax(280px, 0.95fr) minmax(320px, 1fr);
gap: 42px;
grid-template-columns: minmax(280px, 0.9fr) minmax(340px, 1fr);
gap: 28px;
align-items: start;
}
@@ -22,7 +22,7 @@
.product-engagement {
display: grid;
gap: 16px;
gap: 14px;
}
.product-tab-content {
@@ -32,7 +32,7 @@
}
.product-engagement-stacked {
gap: 20px;
gap: 16px;
}
.product-details-loading,
@@ -91,12 +91,28 @@
@media (max-width: 860px) {
.product-details-page {
padding: 16px;
gap: 32px;
padding: 18px;
gap: 24px;
}
.product-details-layout {
grid-template-columns: 1fr;
gap: 24px;
gap: 18px;
}
.product-details-main {
gap: 16px;
}
}
@media (max-width: 640px) {
.product-details-page {
padding: 14px;
gap: 20px;
}
.product-details-message {
min-height: 320px;
padding: 0 6px;
}
}

View File

@@ -7,6 +7,7 @@ import { ConfigService } from '../../../../core/config/config.service';
import { ProductFacade } from '../../../../facades/platform/product.facade';
import { UserExperienceFacade } from '../../../../facades/platform/user-experience.facade';
import { TranslatePipe } from '../../../../i18n/translate.pipe';
import { TranslateService } from '../../../../i18n/translate.service';
import { LangRoutePipe } from '../../../../pipes/lang-route.pipe';
import { CartService } from '../../../../services';
import { LanguageService } from '../../../../services/language.service';
@@ -56,6 +57,7 @@ export class ProductDetailsContainerComponent {
private readonly uxFacade = inject(UserExperienceFacade);
private readonly cartService = inject(CartService);
private readonly languageService = inject(LanguageService);
private readonly translate = inject(TranslateService);
readonly productPageConfigState = signal<Required<ProductPageConfig>>(this.resolveProductPageConfig());
readonly userExperienceConfig = signal(this.resolveUserExperienceConfig());
@@ -422,17 +424,17 @@ export class ProductDetailsContainerComponent {
private getTabLabel(key: ProductTabKey): string {
switch (key) {
case 'description':
return 'Description';
return this.translate.t('itemDetail.description');
case 'specifications':
return 'Specifications';
return this.translate.t('itemDetail.specifications');
case 'reviews':
return 'Reviews';
return this.translate.t('itemDetail.reviews');
case 'questions':
return 'Questions';
return this.translate.t('itemDetail.qna');
case 'delivery':
return 'Delivery';
return this.translate.t('cart.deliveryLabel');
case 'warranty':
return 'Warranty';
return this.translate.t('footer.guarantee');
default:
return key;
}

View File

@@ -1,5 +1,5 @@
@if (tabs.length > 0) {
<nav class="product-tabs" aria-label="Product tabs">
<nav class="product-tabs" [attr.aria-label]="'productDetails.tabsAria' | translate">
@for (tab of tabs; track tab.key) {
<button
type="button"

View File

@@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { TranslatePipe } from '../../../../../../i18n/translate.pipe';
export type ProductTabKey = 'description' | 'specifications' | 'reviews' | 'questions' | 'delivery' | 'warranty';
@@ -12,6 +13,7 @@ export interface ProductTabItem {
@Component({
selector: 'app-product-tabs',
standalone: true,
imports: [TranslatePipe],
templateUrl: './product-tabs.component.html',
styleUrls: ['./product-tabs.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -12,9 +12,9 @@
<div class="answer">
<div class="answer-meta">
<strong>{{ answer.author }}</strong>
<span class="seller-pill" [class.visible]="answer.isOfficialSeller">Official seller</span>
<span class="seller-pill" [class.visible]="answer.isOfficialSeller">{{ 'productDetails.officialSeller' | translate }}</span>
@if (answer.isAccepted) {
<span class="accepted-pill">Accepted answer</span>
<span class="accepted-pill">{{ 'productDetails.acceptedAnswer' | translate }}</span>
}
</div>
<p>{{ answer.text }}</p>
@@ -24,7 +24,7 @@
}
<footer>
<button type="button" disabled>Like ({{ question.likes }})</button>
<button type="button" disabled>Dislike ({{ question.dislikes }})</button>
<button type="button" disabled>{{ 'productDetails.like' | translate }} ({{ question.likes }})</button>
<button type="button" disabled>{{ 'productDetails.dislike' | translate }} ({{ question.dislikes }})</button>
</footer>
</article>

View File

@@ -1,11 +1,12 @@
import { DatePipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Question } from '../../../../../../core/products/models/product-engagement.model';
import { TranslatePipe } from '../../../../../../i18n/translate.pipe';
@Component({
selector: 'app-question-card',
standalone: true,
imports: [DatePipe],
imports: [DatePipe, TranslatePipe],
templateUrl: './question-card.component.html',
styleUrls: ['./question-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -1,19 +1,19 @@
<form class="question-form card" (ngSubmit)="onSubmit()">
<h3>Ask a question</h3>
<h3>{{ 'productDetails.questionFormTitle' | translate }}</h3>
<textarea
id="question-text"
name="text"
rows="4"
[(ngModel)]="text"
placeholder="Ask about size, delivery, compatibility or usage"></textarea>
[placeholder]="'productDetails.questionFormPlaceholder' | translate"></textarea>
<label class="check">
<input type="checkbox" name="anonymous" [(ngModel)]="anonymous" />
<span>Ask anonymously</span>
<span>{{ 'productDetails.questionFormAnonymous' | translate }}</span>
</label>
<button type="submit" [disabled]="submitting || !text.trim()">
@if (submitting) { Submitting... } @else { Submit question }
@if (submitting) { {{ 'productDetails.submitting' | translate }} } @else { {{ 'productDetails.submitQuestion' | translate }} }
</button>
</form>

View File

@@ -1,11 +1,12 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SubmitQuestionInput } from '../../../../../../core/products/models/product-engagement.model';
import { TranslatePipe } from '../../../../../../i18n/translate.pipe';
@Component({
selector: 'app-question-form',
standalone: true,
imports: [FormsModule],
imports: [FormsModule, TranslatePipe],
templateUrl: './question-form.component.html',
styleUrls: ['./question-form.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -1,6 +1,6 @@
<section class="question-list section">
<button type="button" class="ask-btn" (click)="askFormVisible.set(!askFormVisible())">
@if (askFormVisible()) { Hide question form } @else { Ask question }
@if (askFormVisible()) { {{ 'productDetails.hideQuestionForm' | translate }} } @else { {{ 'productDetails.askQuestion' | translate }} }
</button>
@if (askFormVisible()) {
@@ -19,14 +19,14 @@
}
</div>
} @else {
<p class="empty">No questions yet.</p>
<p class="empty">{{ 'productDetails.questionsEmpty' | translate }}</p>
}
@if (result && totalPages > 1) {
<div class="pager">
<button type="button" (click)="previousPage()" [disabled]="result.page <= 1">Previous</button>
<span>Page {{ result.page }} / {{ totalPages }}</span>
<button type="button" (click)="nextPage()" [disabled]="result.page >= totalPages">Next</button>
<button type="button" (click)="previousPage()" [disabled]="result.page <= 1">{{ 'productDetails.previous' | translate }}</button>
<span>{{ 'productDetails.pageOf' | translate:{ page: result.page, total: totalPages } }}</span>
<button type="button" (click)="nextPage()" [disabled]="result.page >= totalPages">{{ 'productDetails.next' | translate }}</button>
</div>
}
</section>

View File

@@ -12,6 +12,13 @@
color: var(--text-primary);
padding: 0 14px;
font-weight: 700;
cursor: pointer;
transition: border-color 0.2s ease, transform 0.2s ease;
}
.ask-btn:hover {
border-color: var(--primary-color);
transform: translateY(-1px);
}
.question-items {
@@ -37,6 +44,7 @@
border-radius: var(--radius-sm);
background: var(--bg-primary);
padding: 0 12px;
cursor: pointer;
}
.skeleton-list {
@@ -55,3 +63,19 @@
@keyframes shimmer {
to { background-position: -200% 0; }
}
@media (max-width: 640px) {
.ask-btn {
width: 100%;
justify-self: stretch;
}
.pager {
display: grid;
grid-template-columns: 1fr;
}
.pager span {
text-align: center;
}
}

View File

@@ -1,12 +1,13 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, signal } from '@angular/core';
import { EngagementListResult, Question, SubmitQuestionInput } from '../../../../../../core/products/models/product-engagement.model';
import { TranslatePipe } from '../../../../../../i18n/translate.pipe';
import { QuestionCardComponent } from '../question-card/question-card.component';
import { QuestionFormComponent } from '../question-form/question-form.component';
@Component({
selector: 'app-question-list',
standalone: true,
imports: [QuestionCardComponent, QuestionFormComponent],
imports: [QuestionCardComponent, QuestionFormComponent, TranslatePipe],
templateUrl: './question-list.component.html',
styleUrls: ['./question-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -10,7 +10,7 @@
<div class="headline">
<strong class="average">{{ summary.average | number:'1.1-1' }}</strong>
<app-stars [rating]="summary.average" size="lg" />
<span class="total">{{ summary.totalReviews }} reviews</span>
<span class="total">{{ 'productDetails.reviewsCountLabel' | translate:{ count: summary.totalReviews } }}</span>
</div>
<div class="distribution">

View File

@@ -1,12 +1,13 @@
import { DecimalPipe, PercentPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { RatingSummary } from '../../../../../../core/products/models/product-engagement.model';
import { TranslatePipe } from '../../../../../../i18n/translate.pipe';
import { StarsComponent } from '../stars/stars.component';
@Component({
selector: 'app-rating-summary',
standalone: true,
imports: [DecimalPipe, PercentPipe, StarsComponent],
imports: [DecimalPipe, PercentPipe, StarsComponent, TranslatePipe],
templateUrl: './rating-summary.component.html',
styleUrls: ['./rating-summary.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -2,7 +2,7 @@
<header class="review-header">
<app-stars [rating]="review.rating" size="sm" />
<strong class="author">{{ review.author }}</strong>
<span class="pill">Verified purchase</span>
<span class="pill">{{ 'productDetails.verifiedPurchase' | translate }}</span>
<time>{{ review.createdAt | date:'mediumDate' }}</time>
</header>
@@ -13,7 +13,7 @@
<p>{{ review.text }}</p>
<footer class="review-actions">
<button type="button" disabled>Like ({{ review.likes }})</button>
<button type="button" disabled>Dislike ({{ review.dislikes }})</button>
<button type="button" disabled>{{ 'productDetails.like' | translate }} ({{ review.likes }})</button>
<button type="button" disabled>{{ 'productDetails.dislike' | translate }} ({{ review.dislikes }})</button>
</footer>
</article>

View File

@@ -1,12 +1,13 @@
import { DatePipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Review } from '../../../../../../core/products/models/product-engagement.model';
import { TranslatePipe } from '../../../../../../i18n/translate.pipe';
import { StarsComponent } from '../stars/stars.component';
@Component({
selector: 'app-review-card',
standalone: true,
imports: [DatePipe, StarsComponent],
imports: [DatePipe, StarsComponent, TranslatePipe],
templateUrl: './review-card.component.html',
styleUrls: ['./review-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -1,23 +1,23 @@
<form class="review-form card" (ngSubmit)="onSubmit()">
<h3>Leave a review</h3>
<h3>{{ 'productDetails.reviewFormTitle' | translate }}</h3>
<label>Rating</label>
<label>{{ 'productDetails.reviewRatingLabel' | translate }}</label>
<app-star-selector [(rating)]="rating" />
<label for="review-title">Title</label>
<input id="review-title" name="title" [(ngModel)]="title" placeholder="Short headline" maxlength="120" />
<label for="review-title">{{ 'productDetails.reviewTitleLabel' | translate }}</label>
<input id="review-title" name="title" [(ngModel)]="title" [placeholder]="'productDetails.reviewTitlePlaceholder' | translate" maxlength="120" />
<label for="review-text">Review</label>
<textarea id="review-text" name="text" [(ngModel)]="text" rows="4" placeholder="Describe your experience"></textarea>
<label for="review-text">{{ 'productDetails.reviewTextLabel' | translate }}</label>
<textarea id="review-text" name="text" [(ngModel)]="text" rows="4" [placeholder]="'productDetails.reviewTextPlaceholder' | translate"></textarea>
<label class="check">
<input type="checkbox" name="anonymous" [(ngModel)]="anonymous" />
<span>Submit anonymously</span>
<span>{{ 'productDetails.reviewAnonymous' | translate }}</span>
</label>
<p class="upload-placeholder">Photo upload is planned for a future sprint.</p>
<p class="upload-placeholder">{{ 'productDetails.reviewUploadPlaceholder' | translate }}</p>
<button type="submit" [disabled]="submitting || !rating || !text.trim()">
@if (submitting) { Submitting... } @else { Submit review }
@if (submitting) { {{ 'productDetails.submitting' | translate }} } @else { {{ 'productDetails.submitReview' | translate }} }
</button>
</form>

View File

@@ -1,12 +1,13 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SubmitReviewInput } from '../../../../../../core/products/models/product-engagement.model';
import { TranslatePipe } from '../../../../../../i18n/translate.pipe';
import { StarSelectorComponent } from '../star-selector/star-selector.component';
@Component({
selector: 'app-review-form',
standalone: true,
imports: [FormsModule, StarSelectorComponent],
imports: [FormsModule, StarSelectorComponent, TranslatePipe],
templateUrl: './review-form.component.html',
styleUrls: ['./review-form.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -17,14 +17,14 @@
}
</div>
} @else {
<p class="empty">No reviews yet.</p>
<p class="empty">{{ 'productDetails.reviewsEmpty' | translate }}</p>
}
@if (result && totalPages > 1) {
<div class="pager">
<button type="button" (click)="previousPage()" [disabled]="result.page <= 1">Previous</button>
<span>Page {{ result.page }} / {{ totalPages }}</span>
<button type="button" (click)="nextPage()" [disabled]="result.page >= totalPages">Next</button>
<button type="button" (click)="previousPage()" [disabled]="result.page <= 1">{{ 'productDetails.previous' | translate }}</button>
<span>{{ 'productDetails.pageOf' | translate:{ page: result.page, total: totalPages } }}</span>
<button type="button" (click)="nextPage()" [disabled]="result.page >= totalPages">{{ 'productDetails.next' | translate }}</button>
</div>
}
</section>

View File

@@ -27,6 +27,12 @@
background: var(--bg-primary);
padding: 0 12px;
cursor: pointer;
transition: border-color 0.2s ease, transform 0.2s ease;
}
.pager button:hover:not(:disabled) {
border-color: var(--primary-color);
transform: translateY(-1px);
}
.skeleton-list {
@@ -45,3 +51,14 @@
@keyframes shimmer {
to { background-position: -200% 0; }
}
@media (max-width: 640px) {
.pager {
display: grid;
grid-template-columns: 1fr;
}
.pager span {
text-align: center;
}
}

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { EngagementListResult, Review, SubmitReviewInput } from '../../../../../../core/products/models/product-engagement.model';
import { RatingSummary } from '../../../../../../core/products/models/product-engagement.model';
import { TranslatePipe } from '../../../../../../i18n/translate.pipe';
import { RatingSummaryComponent } from '../rating-summary/rating-summary.component';
import { ReviewCardComponent } from '../review-card/review-card.component';
import { ReviewFormComponent } from '../review-form/review-form.component';
@@ -8,7 +9,7 @@ import { ReviewFormComponent } from '../review-form/review-form.component';
@Component({
selector: 'app-review-list',
standalone: true,
imports: [RatingSummaryComponent, ReviewCardComponent, ReviewFormComponent],
imports: [RatingSummaryComponent, ReviewCardComponent, ReviewFormComponent, TranslatePipe],
templateUrl: './review-list.component.html',
styleUrls: ['./review-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -1,4 +1,4 @@
<div class="star-selector" role="radiogroup" aria-label="Select rating">
<div class="star-selector" role="radiogroup" [attr.aria-label]="'productDetails.selectRatingAria' | translate">
@for (star of stars; track star) {
<button
type="button"

View File

@@ -1,8 +1,10 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { TranslatePipe } from '../../../../../../i18n/translate.pipe';
@Component({
selector: 'app-star-selector',
standalone: true,
imports: [TranslatePipe],
templateUrl: './star-selector.component.html',
styleUrls: ['./star-selector.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -1,4 +1,4 @@
<span class="stars" [class.sm]="size === 'sm'" [class.lg]="size === 'lg'" aria-label="Rating stars">
<span class="stars" [class.sm]="size === 'sm'" [class.lg]="size === 'lg'" [attr.aria-label]="'productDetails.ratingStarsAria' | translate">
@for (star of stars; track star) {
<span class="star" [class.filled]="star <= rating"></span>
}

View File

@@ -1,8 +1,10 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { TranslatePipe } from '../../../../../../i18n/translate.pipe';
@Component({
selector: 'app-stars',
standalone: true,
imports: [TranslatePipe],
templateUrl: './stars.component.html',
styleUrls: ['./stars.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush