502 lines
20 KiB
HTML
502 lines
20 KiB
HTML
@if (isnovo) {
|
||
<!-- novo VERSION - Modern Design -->
|
||
<div class="novo-item-container">
|
||
@if (loading()) {
|
||
<div class="novo-loading">
|
||
<div class="novo-spinner"></div>
|
||
<p>{{ 'itemDetail.loading' | translate }}</p>
|
||
</div>
|
||
}
|
||
|
||
@if (error()) {
|
||
<div class="novo-error">
|
||
<p>{{ error() }}</p>
|
||
<a [routerLink]="'/' | langRoute" class="novo-back-link">{{ 'itemDetail.back' | translate }}</a>
|
||
</div>
|
||
}
|
||
|
||
@if (item() && !loading()) {
|
||
<div class="novo-item-content">
|
||
<div class="novo-gallery">
|
||
@if (item()?.photos && item()!.photos!.length > 0) {
|
||
<div class="novo-main-photo">
|
||
@if (item()!.photos![selectedPhotoIndex()]?.video) {
|
||
<video [src]="item()!.photos![selectedPhotoIndex()].url" controls></video>
|
||
} @else {
|
||
<img [src]="item()!.photos![selectedPhotoIndex()].url" [alt]="item()!.name" />
|
||
}
|
||
</div>
|
||
|
||
@if (item()!.photos!.length > 1) {
|
||
<div class="novo-thumbnails">
|
||
@for (photo of item()!.photos!; track $index) {
|
||
<div
|
||
class="novo-thumb"
|
||
[class.active]="selectedPhotoIndex() === $index"
|
||
(click)="selectPhoto($index)">
|
||
@if (photo.video) {
|
||
<div class="video-indicator">▶</div>
|
||
}
|
||
<img [src]="photo.url" [alt]="'Photo ' + ($index + 1)" loading="lazy" />
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
} @else {
|
||
<div class="novo-main-photo novo-no-image">
|
||
<svg width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
|
||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||
<path d="M21 15l-5-5L5 21"></path>
|
||
</svg>
|
||
<p>{{ 'itemDetail.noImage' | translate }}</p>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="novo-info">
|
||
<h1 class="novo-title">{{ getItemName() }}</h1>
|
||
|
||
@if (item()!.badges && item()!.badges!.length > 0) {
|
||
<div class="novo-badges">
|
||
@for (badge of item()!.badges!; track badge) {
|
||
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
@if (item()!.tags && item()!.tags!.length > 0) {
|
||
<div class="novo-tags">
|
||
@for (tag of item()!.tags!; track tag) {
|
||
<span class="item-tag">#{{ tag }}</span>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
<div class="novo-rating">
|
||
<span class="stars">{{ getRatingStars(item()!.rating) }}</span>
|
||
<span class="value">{{ item()!.rating }}</span>
|
||
<span class="reviews">({{ item()!.callbacks?.length || 0 }})</span>
|
||
</div>
|
||
|
||
<div class="novo-price-block">
|
||
@if (item()!.discount > 0) {
|
||
<div class="price-row">
|
||
<span class="old-price">{{ item()!.price }} {{ item()!.currency }}</span>
|
||
<span class="discount-badge">-{{ item()!.discount }}%</span>
|
||
</div>
|
||
<div class="current-price">{{ getDiscountedPrice() | number:'1.2-2' }} {{ item()!.currency }}</div>
|
||
} @else {
|
||
<div class="current-price">{{ item()!.price }} {{ item()!.currency }}</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="novo-stock">
|
||
<span class="stock-label">{{ 'itemDetail.stock' | translate }}</span>
|
||
<div class="stock-indicator" [class]="getStockClass()">
|
||
<span class="dot"></span>
|
||
{{ getStockLabel() }}
|
||
</div>
|
||
@if (item()!.quantity != null) {
|
||
<span class="stock-qty">({{ item()!.quantity }} шт.)</span>
|
||
}
|
||
</div>
|
||
|
||
<button class="novo-add-cart" (click)="addToCart()">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="9" cy="21" r="1"></circle>
|
||
<circle cx="20" cy="21" r="1"></circle>
|
||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
||
</svg>
|
||
{{ 'itemDetail.addToCart' | translate }}
|
||
</button>
|
||
|
||
<div class="novo-description">
|
||
@if (getSimpleDescription()) {
|
||
<p class="novo-simple-desc">{{ getSimpleDescription() }}</p>
|
||
}
|
||
|
||
@if (hasDescriptionFields()) {
|
||
<h3>{{ 'itemDetail.specifications' | translate }}</h3>
|
||
<table class="novo-specs-table">
|
||
<tbody>
|
||
@for (field of getTranslatedDescriptionFields(); track field.key) {
|
||
<tr>
|
||
<td class="spec-key">{{ field.key }}</td>
|
||
<td class="spec-value">{{ field.value }}</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
} @else {
|
||
<h3>{{ 'itemDetail.description' | translate }}</h3>
|
||
<div [innerHTML]="getSafeHtml(item()!.description)"></div>
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="novo-reviews">
|
||
<h2>{{ 'itemDetail.reviews' | translate }} ({{ item()!.callbacks?.length || 0 }})</h2>
|
||
|
||
<!-- novo Review Form -->
|
||
<div class="novo-review-form">
|
||
<h3>{{ 'itemDetail.yourReview' | translate }}</h3>
|
||
<div class="novo-rating-input">
|
||
<label>{{ 'itemDetail.rating' | translate }}</label>
|
||
<div class="novo-star-selector">
|
||
@for (star of [1, 2, 3, 4, 5]; track star) {
|
||
<span
|
||
class="novo-star"
|
||
[class.selected]="newReview.rating >= star"
|
||
(click)="setRating(star)">
|
||
★
|
||
</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
<textarea
|
||
[(ngModel)]="newReview.comment"
|
||
[placeholder]="'itemDetail.reviewPlaceholder' | translate"
|
||
rows="4"
|
||
class="novo-textarea">
|
||
</textarea>
|
||
<div class="novo-form-actions">
|
||
<label class="novo-anonymous-toggle">
|
||
<input type="checkbox" [(ngModel)]="newReview.anonymous">
|
||
<span>{{ 'itemDetail.anonymous' | translate }}</span>
|
||
</label>
|
||
@if (!newReview.anonymous && getUserDisplayName()) {
|
||
<span class="novo-username-preview">{{ getUserDisplayName() }}</span>
|
||
}
|
||
<button
|
||
class="novo-submit-review-btn"
|
||
(click)="submitReview()"
|
||
[disabled]="!newReview.rating || !newReview.comment.trim() || reviewSubmitStatus() === 'loading'"
|
||
[class.submitting]="reviewSubmitStatus() === 'loading'">
|
||
@if (reviewSubmitStatus() === 'loading') {
|
||
<span class="novo-spinner-small"></span>
|
||
{{ 'itemDetail.submitting' | translate }}
|
||
} @else {
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
|
||
</svg>
|
||
{{ 'itemDetail.submit' | translate }}
|
||
}
|
||
</button>
|
||
</div>
|
||
|
||
@if (reviewSubmitStatus() === 'success') {
|
||
<div class="novo-status-message success">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<path d="M20 6L9 17l-5-5"/>
|
||
</svg>
|
||
{{ 'itemDetail.reviewSuccess' | translate }}
|
||
</div>
|
||
}
|
||
|
||
@if (reviewSubmitStatus() === 'error') {
|
||
<div class="novo-status-message error">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||
<path d="M18 6L6 18M6 6l12 12"/>
|
||
</svg>
|
||
{{ 'itemDetail.reviewError' | translate }}
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="novo-reviews-list">
|
||
@if (item()!.callbacks && item()!.callbacks!.length > 0) {
|
||
@for (review of item()!.callbacks!; track review.userID) {
|
||
<div class="novo-review-card">
|
||
<div class="review-header">
|
||
<div class="reviewer-info">
|
||
<span class="reviewer-name">{{ review.userID ? review.userID : ('itemDetail.defaultUser' | translate) }}</span>
|
||
@if (review.timestamp) {
|
||
<span class="review-date">{{ formatDate(review.timestamp) }}</span>
|
||
}
|
||
</div>
|
||
<span class="review-stars">{{ getRatingStars(review.rating || 0) }}</span>
|
||
</div>
|
||
<p class="review-text">{{ review.content }}</p>
|
||
</div>
|
||
}
|
||
} @else {
|
||
<p class="novo-no-reviews">{{ 'itemDetail.noReviews' | translate }}</p>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
} @else {
|
||
<!-- DEXAR VERSION - Redesigned 2026 -->
|
||
<div class="dx-item-container">
|
||
@if (loading()) {
|
||
<div class="dx-loading">
|
||
<div class="dx-spinner"></div>
|
||
<p>{{ 'itemDetail.loadingDexar' | translate }}</p>
|
||
</div>
|
||
}
|
||
|
||
@if (error()) {
|
||
<div class="dx-error">
|
||
<p>{{ error() }}</p>
|
||
<a [routerLink]="'/' | langRoute" class="dx-back-link">{{ 'itemDetail.backHome' | translate }}</a>
|
||
</div>
|
||
}
|
||
|
||
@if (item() && !loading()) {
|
||
<div class="dx-item-content">
|
||
<!-- Gallery: thumbnails left + main photo -->
|
||
<div class="dx-gallery">
|
||
@if (item()?.photos && item()!.photos!.length > 0) {
|
||
<div class="dx-thumbnails">
|
||
@for (photo of item()!.photos!; track $index) {
|
||
<div
|
||
class="dx-thumb"
|
||
[class.active]="selectedPhotoIndex() === $index"
|
||
(click)="selectPhoto($index)">
|
||
@if (photo.video) {
|
||
<div class="dx-video-badge">▶</div>
|
||
}
|
||
<img [src]="photo.url" [alt]="('itemDetail.photo' | translate) + ' ' + ($index + 1)" loading="lazy" decoding="async" />
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
<div class="dx-main-photo">
|
||
@if (item()?.photos && item()!.photos!.length > 0) {
|
||
@if (item()!.photos![selectedPhotoIndex()]?.video) {
|
||
<video [src]="item()!.photos![selectedPhotoIndex()].url" controls></video>
|
||
} @else {
|
||
<img [src]="item()!.photos![selectedPhotoIndex()].url" [alt]="item()!.name" fetchpriority="high" decoding="async" />
|
||
}
|
||
} @else {
|
||
<div class="dx-no-image">
|
||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#a1b4b5" stroke-width="1.5">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
|
||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||
<path d="M21 15l-5-5L5 21"></path>
|
||
</svg>
|
||
<span>{{ 'itemDetail.noImage' | translate }}</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Item Info -->
|
||
<div class="dx-info">
|
||
<h1 class="dx-title">{{ getItemName() }}</h1>
|
||
|
||
@if (item()!.badges && item()!.badges!.length > 0) {
|
||
<div class="dx-badges">
|
||
@for (badge of item()!.badges!; track badge) {
|
||
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
@if (item()!.tags && item()!.tags!.length > 0) {
|
||
<div class="dx-tags">
|
||
@for (tag of item()!.tags!; track tag) {
|
||
<span class="item-tag">#{{ tag }}</span>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
<div class="dx-rating">
|
||
<div class="dx-stars">
|
||
@for (star of [1, 2, 3, 4, 5]; track star) {
|
||
<svg width="18" height="18" viewBox="0 0 24 24" [attr.fill]="star <= item()!.rating ? '#497671' : 'none'" [attr.stroke]="star <= item()!.rating ? '#497671' : '#a1b4b5'" stroke-width="2">
|
||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
|
||
</svg>
|
||
}
|
||
</div>
|
||
<span class="dx-rating-value">{{ item()!.rating }}</span>
|
||
<span class="dx-rating-count">({{ item()!.callbacks?.length || 0 }} {{ 'itemDetail.reviewsCount' | translate }})</span>
|
||
</div>
|
||
|
||
<div class="dx-price-block">
|
||
@if (item()!.discount > 0) {
|
||
<div class="dx-price-row">
|
||
<span class="dx-old-price">{{ item()!.price }} {{ item()!.currency }}</span>
|
||
<span class="dx-discount-tag">-{{ item()!.discount }}%</span>
|
||
</div>
|
||
}
|
||
<div class="dx-current-price">
|
||
{{ item()!.discount > 0 ? (getDiscountedPrice() | number:'1.2-2') : item()!.price }} {{ item()!.currency }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="dx-stock">
|
||
<span class="dx-stock-label">{{ 'itemDetail.stock' | translate }}</span>
|
||
<span class="dx-stock-status" [class]="getStockClass()">
|
||
<span class="dx-stock-dot"></span>
|
||
{{ getStockLabel() }}
|
||
</span>
|
||
@if (item()!.quantity != null) {
|
||
<span class="dx-stock-qty">({{ item()!.quantity }} шт.)</span>
|
||
}
|
||
</div>
|
||
|
||
<button class="dx-add-cart" (click)="addToCart()">
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="9" cy="21" r="1"></circle>
|
||
<circle cx="20" cy="21" r="1"></circle>
|
||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
||
</svg>
|
||
{{ 'itemDetail.addToCart' | translate }}
|
||
</button>
|
||
|
||
<div class="dx-description">
|
||
@if (getSimpleDescription()) {
|
||
<p class="dx-simple-desc">{{ getSimpleDescription() }}</p>
|
||
}
|
||
|
||
@if (hasDescriptionFields()) {
|
||
<h2>{{ 'itemDetail.specifications' | translate }}</h2>
|
||
<table class="dx-specs-table">
|
||
<tbody>
|
||
@for (field of getTranslatedDescriptionFields(); track field.key) {
|
||
<tr>
|
||
<td class="spec-key">{{ field.key }}</td>
|
||
<td class="spec-value">{{ field.value }}</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
} @else {
|
||
<h2>{{ 'itemDetail.description' | translate }}</h2>
|
||
<div class="dx-description-text" [innerHTML]="getSafeHtml(item()!.description)"></div>
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Reviews Section -->
|
||
<div class="dx-reviews-section">
|
||
<h2>{{ 'itemDetail.reviews' | translate }} ({{ item()!.callbacks?.length || 0 }})</h2>
|
||
|
||
<div class="dx-review-form">
|
||
<h3>{{ 'itemDetail.leaveReview' | translate }}</h3>
|
||
<div class="dx-rating-input">
|
||
<label>{{ 'itemDetail.rating' | translate }}</label>
|
||
<div class="dx-star-selector">
|
||
@for (star of [1, 2, 3, 4, 5]; track star) {
|
||
<span
|
||
class="dx-star-pick"
|
||
[class.selected]="newReview.rating >= star"
|
||
(click)="setRating(star)">
|
||
★
|
||
</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
<textarea
|
||
[(ngModel)]="newReview.comment"
|
||
[placeholder]="'itemDetail.reviewPlaceholderDexar' | translate"
|
||
rows="4"
|
||
class="dx-textarea">
|
||
</textarea>
|
||
<div class="dx-form-actions">
|
||
<label class="dx-anon-toggle">
|
||
<input type="checkbox" [(ngModel)]="newReview.anonymous">
|
||
<span>{{ 'itemDetail.anonymous' | translate }}</span>
|
||
</label>
|
||
@if (!newReview.anonymous && getUserDisplayName()) {
|
||
<span class="dx-user-preview">{{ getUserDisplayName() }}</span>
|
||
}
|
||
<button
|
||
class="dx-submit-btn"
|
||
(click)="submitReview()"
|
||
[disabled]="!newReview.rating || !newReview.comment.trim() || reviewSubmitStatus() === 'loading'"
|
||
[class.submitting]="reviewSubmitStatus() === 'loading'">
|
||
@if (reviewSubmitStatus() === 'loading') {
|
||
<span class="dx-spinner-sm"></span>
|
||
{{ 'itemDetail.submitting' | translate }}
|
||
} @else {
|
||
{{ 'itemDetail.submit' | translate }}
|
||
}
|
||
</button>
|
||
</div>
|
||
|
||
@if (reviewSubmitStatus() === 'success') {
|
||
<div class="dx-status success">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
||
{{ 'itemDetail.reviewSuccess' | translate }}
|
||
</div>
|
||
}
|
||
|
||
@if (reviewSubmitStatus() === 'error') {
|
||
<div class="dx-status error">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||
{{ 'itemDetail.reviewError' | translate }}
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="dx-reviews-list">
|
||
@if (item()?.callbacks && item()!.callbacks!.length > 0) {
|
||
@for (callback of item()!.callbacks; track $index) {
|
||
<div class="dx-review-card">
|
||
<div class="dx-review-header">
|
||
<div class="dx-reviewer">
|
||
<span class="dx-reviewer-name">{{ callback.userID ? callback.userID : ('itemDetail.defaultUser' | translate) }}</span>
|
||
@if (callback.timestamp) {
|
||
<span class="dx-review-date">{{ formatDate(callback.timestamp) }}</span>
|
||
}
|
||
</div>
|
||
@if (callback.rating) {
|
||
<div class="dx-review-stars">
|
||
@for (star of [1, 2, 3, 4, 5]; track star) {
|
||
<svg width="14" height="14" viewBox="0 0 24 24" [attr.fill]="star <= callback.rating ? '#497671' : 'none'" [attr.stroke]="star <= callback.rating ? '#497671' : '#d3dad9'" stroke-width="2">
|
||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
|
||
</svg>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
@if (callback.content) {
|
||
<p class="dx-review-text">{{ callback.content }}</p>
|
||
}
|
||
</div>
|
||
}
|
||
} @else {
|
||
<p class="dx-no-reviews">{{ 'itemDetail.noReviews' | translate }}</p>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Q&A Section -->
|
||
@if (item()!.questions && item()!.questions!.length > 0) {
|
||
<div class="dx-qa-section">
|
||
<h2>{{ 'itemDetail.qna' | translate }} ({{ item()!.questions!.length }})</h2>
|
||
<div class="dx-qa-list">
|
||
@for (question of item()!.questions!; track $index) {
|
||
<div class="dx-qa-card">
|
||
<div class="dx-question">
|
||
<span class="dx-qa-label q">В</span>
|
||
<span>{{ question.question }}</span>
|
||
</div>
|
||
<div class="dx-answer">
|
||
<span class="dx-qa-label a">О</span>
|
||
<span>{{ question.answer }}</span>
|
||
</div>
|
||
<div class="dx-qa-votes">
|
||
<button class="dx-vote up">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14z"/><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>
|
||
{{ question.upvotes }}
|
||
</button>
|
||
<button class="dx-vote down">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10z"/><path d="M17 2h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"/></svg>
|
||
{{ question.downvotes }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
}
|
||
</div>
|
||
} |