bug-fixes
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user