715 lines
35 KiB
Plaintext
715 lines
35 KiB
Plaintext
|
|
@model SkyArtShop.Models.Product
|
||
|
|
@{
|
||
|
|
ViewData["Title"] = Model.Name;
|
||
|
|
}
|
||
|
|
|
||
|
|
<section class="product-detail-modern">
|
||
|
|
<div class="container">
|
||
|
|
<div class="product-split">
|
||
|
|
<!-- LEFT: Gallery -->
|
||
|
|
<div class="image-pane">
|
||
|
|
<div class="gallery">
|
||
|
|
<div class="gallery-sidebar">
|
||
|
|
<div class="gallery-thumbs">
|
||
|
|
@if (Model.Images != null && Model.Images.Count > 0)
|
||
|
|
{
|
||
|
|
@for (int i = 0; i < Model.Images.Count; i++)
|
||
|
|
{
|
||
|
|
var image = Model.Images[i];
|
||
|
|
var isFirst = i == 0;
|
||
|
|
<div class="thumb @(isFirst ? "active" : "")" data-src="@image" onclick="setImage(this)">
|
||
|
|
<img src="@image" alt="@Model.Name">
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else if (!string.IsNullOrEmpty(Model.ImageUrl))
|
||
|
|
{
|
||
|
|
<div class="thumb active" data-src="@Model.ImageUrl" onclick="setImage(this)">
|
||
|
|
<img src="@Model.ImageUrl" alt="@Model.Name">
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
<div class="thumb active" data-src="/assets/images/placeholder.jpg" onclick="setImage(this)">
|
||
|
|
<img src="/assets/images/placeholder.jpg" alt="@Model.Name">
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
<div class="zoom-hint"><i class="bi bi-zoom-in"></i> Click to view full size</div>
|
||
|
|
</div>
|
||
|
|
<div class="gallery-main" onclick="openLightbox()">
|
||
|
|
<button class="nav prev" type="button" onclick="event.stopPropagation(); slideImage(-1)"><i class="bi bi-chevron-left"></i></button>
|
||
|
|
@{
|
||
|
|
var mainImageSrc = Model.Images != null && Model.Images.Count > 0
|
||
|
|
? Model.Images[0]
|
||
|
|
: (!string.IsNullOrEmpty(Model.ImageUrl) ? Model.ImageUrl : "/assets/images/placeholder.jpg");
|
||
|
|
}
|
||
|
|
<img id="galleryImage" src="@mainImageSrc" alt="@Model.Name">
|
||
|
|
<button class="nav next" type="button" onclick="event.stopPropagation(); slideImage(1)"><i class="bi bi-chevron-right"></i></button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- RIGHT: Details -->
|
||
|
|
<div class="info-pane">
|
||
|
|
<div class="details">
|
||
|
|
<h1 class="title">@Model.Name</h1>
|
||
|
|
|
||
|
|
@if (!string.IsNullOrEmpty(Model.ShortDescription))
|
||
|
|
{
|
||
|
|
<p class="short-description">@Model.ShortDescription</p>
|
||
|
|
}
|
||
|
|
|
||
|
|
<div class="meta">
|
||
|
|
<div class="meta-left">
|
||
|
|
@if (!string.IsNullOrEmpty(Model.SKU))
|
||
|
|
{
|
||
|
|
<span class="sku">SKU: @Model.SKU</span>
|
||
|
|
}
|
||
|
|
else if (!string.IsNullOrEmpty(Model.Category))
|
||
|
|
{
|
||
|
|
<span class="sku">SKU: @Model.Category.ToUpper().Replace(" ","")@Model.Id?.Substring(Model.Id.Length - 4)</span>
|
||
|
|
}
|
||
|
|
@{
|
||
|
|
var rating = Model.AverageRating > 0 ? Model.AverageRating : 5.0;
|
||
|
|
var fullStars = (int)Math.Floor(rating);
|
||
|
|
var hasHalfStar = (rating - fullStars) >= 0.5;
|
||
|
|
var emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
|
||
|
|
}
|
||
|
|
<div class="stars">
|
||
|
|
@for (int i = 0; i < fullStars; i++)
|
||
|
|
{
|
||
|
|
<i class="bi bi-star-fill"></i>
|
||
|
|
}
|
||
|
|
@if (hasHalfStar)
|
||
|
|
{
|
||
|
|
<i class="bi bi-star-half"></i>
|
||
|
|
}
|
||
|
|
@for (int i = 0; i < emptyStars; i++)
|
||
|
|
{
|
||
|
|
<i class="bi bi-star"></i>
|
||
|
|
}
|
||
|
|
<span class="rating-text">(@Model.TotalReviews review@(Model.TotalReviews != 1 ? "s" : ""))</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
@if (Model.UnitsSold > 0)
|
||
|
|
{
|
||
|
|
<span class="units-sold">@Model.UnitsSold sold</span>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
<!-- Price first -->
|
||
|
|
<div class="price-row">
|
||
|
|
<span class="label">Price:</span>
|
||
|
|
<span class="price">$@Model.Price.ToString("F2")</span>
|
||
|
|
</div>
|
||
|
|
<!-- Stock info under price -->
|
||
|
|
<div class="stock-row">
|
||
|
|
@if (Model.StockQuantity > 0)
|
||
|
|
{
|
||
|
|
<div class="stock ok"><i class="bi bi-check-circle-fill"></i> In stock (@Model.StockQuantity+
|
||
|
|
units), ready to be shipped</div>
|
||
|
|
<div class="stock-bar green"></div>
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
|
||
|
|
<!-- Actions below quantity and color -->
|
||
|
|
<div class="actions">
|
||
|
|
@if (Model.StockQuantity > 0)
|
||
|
|
{
|
||
|
|
<button class="cta" onclick="addToCartFromDetail()"><i class="bi bi-cart-plus"></i> Add to Cart</button>
|
||
|
|
<button class="cta alt" onclick="addToWishlistFromDetail()"><i class="bi bi-heart"></i> Add to Wishlist</button>
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
<button class="cta" disabled>Out of Stock</button>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
<div class="stock bad"><i class="bi bi-x-circle-fill"></i> Out of stock</div>
|
||
|
|
<div class="stock-bar red"></div>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
<!-- Quantity next -->
|
||
|
|
<div class="qty-row">
|
||
|
|
<div class="qty-header">
|
||
|
|
<span class="label">Quantity:</span>
|
||
|
|
@if (Model.StockQuantity > 0)
|
||
|
|
{
|
||
|
|
<span class="stock-count">(@Model.StockQuantity available)</span>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
<div class="qty">
|
||
|
|
<button type="button" class="qty-btn" onclick="decreaseQuantity()" @(Model.StockQuantity == 0 ?
|
||
|
|
"disabled" : "")><i class="bi bi-dash"></i></button>
|
||
|
|
<input id="quantity" type="number" value="1" min="1" max="@Model.StockQuantity" readonly>
|
||
|
|
<button type="button" class="qty-btn" onclick="increaseQuantity()" @(Model.StockQuantity == 0 ?
|
||
|
|
"disabled" : "")><i class="bi bi-plus"></i></button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<!-- Actions below quantity -->
|
||
|
|
<div class="actions">
|
||
|
|
@if (Model.StockQuantity > 0)
|
||
|
|
{
|
||
|
|
<button class="cta" onclick="addToCartFromDetail()"><i class="bi bi-cart-plus"></i> Add to Cart</button>
|
||
|
|
<button class="cta alt" onclick="addToWishlistFromDetail()"><i class="bi bi-heart"></i> Add to Wishlist</button>
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
<button class="cta" disabled>Out of Stock</button>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Color Variant Selector -->
|
||
|
|
@{
|
||
|
|
var hasVariants = Model.Variants != null && Model.Variants.Any(v => v.IsAvailable && v.Images != null && v.Images.Any());
|
||
|
|
var hasLegacyColors = (Model.Colors != null && Model.Colors.Any()) || !string.IsNullOrEmpty(Model.Color);
|
||
|
|
|
||
|
|
if (hasVariants && Model.Variants != null)
|
||
|
|
{
|
||
|
|
// New variant system - filter only available variants with images
|
||
|
|
var availableVariants = Model.Variants.Where(v => v.IsAvailable && v.Images != null && v.Images.Any()).ToList();
|
||
|
|
|
||
|
|
<div class="color-section variant-section">
|
||
|
|
<div class="color-row">
|
||
|
|
<span class="label">Select Color:</span>
|
||
|
|
<span class="value" id="selectedVariantName">Choose a color</span>
|
||
|
|
</div>
|
||
|
|
<div class="variant-swatches" id="variantSwatches">
|
||
|
|
@foreach (var variant in availableVariants)
|
||
|
|
{
|
||
|
|
var variantJson = System.Text.Json.JsonSerializer.Serialize(new {
|
||
|
|
colorName = variant.ColorName,
|
||
|
|
colorHex = variant.ColorHex,
|
||
|
|
images = variant.Images,
|
||
|
|
stock = variant.StockQuantity,
|
||
|
|
priceAdjust = variant.PriceAdjustment,
|
||
|
|
sku = variant.SKU
|
||
|
|
});
|
||
|
|
|
||
|
|
<div class="variant-swatch"
|
||
|
|
data-variant='@Html.Raw(variantJson)'
|
||
|
|
onclick="selectVariant(this)"
|
||
|
|
title="@variant.ColorName (@variant.StockQuantity in stock)">
|
||
|
|
<span class="variant-dot" style="background-color: @variant.ColorHex; box-shadow: 0 0 0 2px white, 0 0 0 3px #ddd;"></span>
|
||
|
|
<span class="variant-name">@variant.ColorName</span>
|
||
|
|
@if (variant.StockQuantity <= 5 && variant.StockQuantity > 0)
|
||
|
|
{
|
||
|
|
<span class="variant-badge">Only @variant.StockQuantity left</span>
|
||
|
|
}
|
||
|
|
@if (variant.StockQuantity == 0)
|
||
|
|
{
|
||
|
|
<span class="variant-badge out-of-stock">Out of Stock</span>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
<input type="hidden" id="selectedVariantData" value="">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
// Store all variant data for easy access
|
||
|
|
window.productVariants = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(availableVariants));
|
||
|
|
|
||
|
|
// Don't auto-select on page load - show all images initially
|
||
|
|
// User can click a color to filter/highlight
|
||
|
|
</script>
|
||
|
|
}
|
||
|
|
@* Legacy color system removed - all products should use Variants *@
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
@if (!string.IsNullOrEmpty(Model.Description))
|
||
|
|
{
|
||
|
|
<div class="row mt-4">
|
||
|
|
<div class="col-12">
|
||
|
|
<div class="description-container">
|
||
|
|
<div class="description-tab" onclick="toggleDescription()">
|
||
|
|
<span>Description</span>
|
||
|
|
<i class="bi bi-chevron-down" id="descChevron"></i>
|
||
|
|
</div>
|
||
|
|
<div class="description-box" id="descriptionBox">
|
||
|
|
<div class="description-content" id="descriptionContent">
|
||
|
|
@Html.Raw(Model.Description)
|
||
|
|
</div>
|
||
|
|
<button class="see-more-btn" id="seeMoreBtn" onclick="expandDescription()" style="display: none;">
|
||
|
|
<i class="bi bi-chevron-down"></i> See More
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
<!-- Product Description Tabs -->
|
||
|
|
|
||
|
|
|
||
|
|
<!-- Related Products Section -->
|
||
|
|
@if (ViewBag.RelatedProducts != null && ViewBag.RelatedProducts.Count > 0)
|
||
|
|
{
|
||
|
|
<div class="row mt-5">
|
||
|
|
<div class="col-12">
|
||
|
|
<h3 class="section-title mb-3">You May Also Like</h3>
|
||
|
|
<p class="text-muted mb-4">Based on what customers are viewing</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="products-grid mb-4">
|
||
|
|
@foreach (var relatedProduct in ViewBag.RelatedProducts)
|
||
|
|
{
|
||
|
|
<div class="product-card">
|
||
|
|
<a href="/shop/product/@relatedProduct.Id" class="product-link">
|
||
|
|
<div class="product-image">
|
||
|
|
<img src="@(string.IsNullOrEmpty(relatedProduct.ImageUrl) ? "/assets/images/placeholder.jpg" : relatedProduct.ImageUrl)"
|
||
|
|
alt="@relatedProduct.Name" loading="lazy">
|
||
|
|
</div>
|
||
|
|
<h3>@relatedProduct.Name</h3>
|
||
|
|
@if (!string.IsNullOrEmpty(relatedProduct.Color))
|
||
|
|
{
|
||
|
|
<span class="product-color-badge">@relatedProduct.Color</span>
|
||
|
|
}
|
||
|
|
<div class="product-description">@Html.Raw(relatedProduct.ShortDescription ?? relatedProduct.Description)</div>
|
||
|
|
<p class="price">$@relatedProduct.Price.ToString("F2")</p>
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
<div class="row">
|
||
|
|
<div class="col-12 text-center">
|
||
|
|
<a href="/shop?category=@Model.Category" class="btn btn-outline-primary">
|
||
|
|
Browse More @Model.Category Products
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
<div class="row mt-5">
|
||
|
|
<div class="col-12 text-center">
|
||
|
|
<h3 class="section-title mb-3">Explore Our Collection</h3>
|
||
|
|
<a href="/shop?category=@Model.Category" class="btn btn-outline-primary">
|
||
|
|
Browse @Model.Category
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
@section Scripts {
|
||
|
|
<script>
|
||
|
|
// Simple slider/gallery with fade transition + hover zoom
|
||
|
|
const images = [
|
||
|
|
@if (Model.Images != null && Model.Images.Count > 0)
|
||
|
|
{
|
||
|
|
@for (int i = 0; i < Model.Images.Count; i++)
|
||
|
|
{
|
||
|
|
@: '@Model.Images[i]'@(i < Model.Images.Count - 1 ? "," : "")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else if (!string.IsNullOrEmpty(Model.ImageUrl))
|
||
|
|
{
|
||
|
|
@: '@Model.ImageUrl'
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
@: '/assets/images/placeholder.jpg'
|
||
|
|
}
|
||
|
|
];
|
||
|
|
let currentIndex = 0;
|
||
|
|
let animating = false;
|
||
|
|
|
||
|
|
function changeImage(nextSrc, direction = 0) {
|
||
|
|
const img = document.getElementById('galleryImage');
|
||
|
|
if (animating) return;
|
||
|
|
animating = true;
|
||
|
|
// small directional nudge for slide feel
|
||
|
|
const shift = direction === 0 ? 0 : (direction > 0 ? 12 : -12);
|
||
|
|
img.style.transform = `translateX(${shift}px) scale(1)`;
|
||
|
|
// start fade-out
|
||
|
|
img.classList.add('fade-out');
|
||
|
|
const onTransitionEnd = () => {
|
||
|
|
img.removeEventListener('transitionend', onTransitionEnd);
|
||
|
|
img.onload = () => {
|
||
|
|
// fade back in once new image is loaded
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
img.classList.remove('fade-out');
|
||
|
|
img.style.transform = 'scale(1)';
|
||
|
|
animating = false;
|
||
|
|
});
|
||
|
|
};
|
||
|
|
img.src = nextSrc;
|
||
|
|
};
|
||
|
|
// If the browser doesn't fire transitionend (short durations), fallback
|
||
|
|
img.addEventListener('transitionend', onTransitionEnd);
|
||
|
|
// Fallback timeout (safety)
|
||
|
|
setTimeout(() => {
|
||
|
|
if (img.classList.contains('fade-out')) {
|
||
|
|
onTransitionEnd();
|
||
|
|
}
|
||
|
|
}, 220);
|
||
|
|
}
|
||
|
|
|
||
|
|
function setImage(el) {
|
||
|
|
const src = el.getAttribute('data-src');
|
||
|
|
changeImage(src, 0);
|
||
|
|
document.querySelectorAll('.gallery-thumbs .thumb').forEach(t => t.classList.remove('active'));
|
||
|
|
el.classList.add('active');
|
||
|
|
currentIndex = images.indexOf(src);
|
||
|
|
|
||
|
|
// Clear variant filtering when manually clicking an image
|
||
|
|
const gallery = document.querySelector('.gallery');
|
||
|
|
if (gallery) {
|
||
|
|
gallery.classList.remove('variant-filtering');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clear variant selection visual
|
||
|
|
document.querySelectorAll('.variant-swatch').forEach(s => s.classList.remove('selected'));
|
||
|
|
document.querySelectorAll('.variant-dot').forEach(d => {
|
||
|
|
d.style.boxShadow = '0 0 0 2px white, 0 0 0 3px #ddd';
|
||
|
|
});
|
||
|
|
|
||
|
|
// Clear variant match classes
|
||
|
|
document.querySelectorAll('.gallery-thumbs .thumb').forEach(t => t.classList.remove('variant-match'));
|
||
|
|
|
||
|
|
// Clear selected variant data
|
||
|
|
const variantNameEl = document.getElementById('selectedVariantName');
|
||
|
|
if (variantNameEl) {
|
||
|
|
variantNameEl.textContent = 'Choose a color';
|
||
|
|
}
|
||
|
|
document.getElementById('selectedVariantData').value = '';
|
||
|
|
}
|
||
|
|
|
||
|
|
function slideImage(direction) {
|
||
|
|
currentIndex = (currentIndex + direction + images.length) % images.length;
|
||
|
|
const nextSrc = images[currentIndex];
|
||
|
|
changeImage(nextSrc, direction);
|
||
|
|
|
||
|
|
// Clear variant filtering when using arrow navigation
|
||
|
|
const gallery = document.querySelector('.gallery');
|
||
|
|
if (gallery) {
|
||
|
|
gallery.classList.remove('variant-filtering');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clear variant selection visual
|
||
|
|
document.querySelectorAll('.variant-swatch').forEach(s => s.classList.remove('selected'));
|
||
|
|
document.querySelectorAll('.variant-dot').forEach(d => {
|
||
|
|
d.style.boxShadow = '0 0 0 2px white, 0 0 0 3px #ddd';
|
||
|
|
});
|
||
|
|
|
||
|
|
// Clear variant match classes
|
||
|
|
document.querySelectorAll('.gallery-thumbs .thumb').forEach(t => t.classList.remove('variant-match'));
|
||
|
|
|
||
|
|
// Clear selected variant data
|
||
|
|
const variantNameEl = document.getElementById('selectedVariantName');
|
||
|
|
if (variantNameEl) {
|
||
|
|
variantNameEl.textContent = 'Choose a color';
|
||
|
|
}
|
||
|
|
document.getElementById('selectedVariantData').value = '';
|
||
|
|
|
||
|
|
// update active thumb
|
||
|
|
document.querySelectorAll('.gallery-thumbs .thumb').forEach(t => {
|
||
|
|
if (t.getAttribute('data-src') === nextSrc) t.classList.add('active'); else t.classList.remove('active');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function increaseQuantity() {
|
||
|
|
const input = document.getElementById('quantity');
|
||
|
|
const max = parseInt(input.max);
|
||
|
|
const current = parseInt(input.value);
|
||
|
|
if (current < max) {
|
||
|
|
input.value = current + 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function decreaseQuantity() {
|
||
|
|
const input = document.getElementById('quantity');
|
||
|
|
const current = parseInt(input.value);
|
||
|
|
if (current > 1) {
|
||
|
|
input.value = current - 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function addToCartFromDetail() {
|
||
|
|
const quantity = parseInt(document.getElementById('quantity').value);
|
||
|
|
const productId = '@Model.Id';
|
||
|
|
let productName = '@Model.Name';
|
||
|
|
let productPrice = @Model.Price;
|
||
|
|
let imageUrl = '@(Model.Images != null && Model.Images.Count > 0 ? Model.Images[0] : "/assets/images/placeholder.jpg")';
|
||
|
|
|
||
|
|
// Check if a variant is selected
|
||
|
|
const selectedVariantEl = document.getElementById('selectedVariantData');
|
||
|
|
if (selectedVariantEl && selectedVariantEl.value) {
|
||
|
|
try {
|
||
|
|
const variantData = JSON.parse(selectedVariantEl.value);
|
||
|
|
|
||
|
|
// Append color to product name
|
||
|
|
productName += ` (${variantData.colorName})`;
|
||
|
|
|
||
|
|
// Apply price adjustment if any
|
||
|
|
if (variantData.priceAdjust) {
|
||
|
|
productPrice += variantData.priceAdjust;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Use variant's first image if available
|
||
|
|
if (variantData.images && variantData.images.length > 0) {
|
||
|
|
imageUrl = variantData.images[0];
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Error parsing variant data:', e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Call the cart function multiple times for quantity
|
||
|
|
for (let i = 0; i < quantity; i++) {
|
||
|
|
addToCart(productId, productName, productPrice, imageUrl);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Animate cart icon
|
||
|
|
const cartBtn = document.getElementById('cartBtn');
|
||
|
|
const cartBadge = cartBtn ? cartBtn.querySelector('.badge') : null;
|
||
|
|
|
||
|
|
if (cartBtn) {
|
||
|
|
cartBtn.style.animation = 'cartBounce 0.6s ease';
|
||
|
|
setTimeout(() => {
|
||
|
|
cartBtn.style.animation = '';
|
||
|
|
}, 600);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (cartBadge) {
|
||
|
|
cartBadge.style.animation = 'badgePulse 0.6s ease';
|
||
|
|
setTimeout(() => {
|
||
|
|
cartBadge.style.animation = '';
|
||
|
|
}, 600);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Show success toast
|
||
|
|
showSuccessToast(`Added ${quantity} x ${productName} to cart!`);
|
||
|
|
}
|
||
|
|
|
||
|
|
function addToWishlistFromDetail() {
|
||
|
|
const productId = '@Model.Id';
|
||
|
|
let productName = '@Model.Name';
|
||
|
|
let productPrice = @Model.Price;
|
||
|
|
let imageUrl = '@(Model.Images != null && Model.Images.Count > 0 ? Model.Images[0] : "/assets/images/placeholder.jpg")';
|
||
|
|
|
||
|
|
// Check if a variant is selected
|
||
|
|
const selectedVariantEl = document.getElementById('selectedVariantData');
|
||
|
|
if (selectedVariantEl && selectedVariantEl.value) {
|
||
|
|
try {
|
||
|
|
const variantData = JSON.parse(selectedVariantEl.value);
|
||
|
|
|
||
|
|
// Append color to product name
|
||
|
|
productName += ` (${variantData.colorName})`;
|
||
|
|
|
||
|
|
// Apply price adjustment if any
|
||
|
|
if (variantData.priceAdjust) {
|
||
|
|
productPrice += variantData.priceAdjust;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Use variant's first image if available
|
||
|
|
if (variantData.images && variantData.images.length > 0) {
|
||
|
|
imageUrl = variantData.images[0];
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Error parsing variant data:', e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
addToWishlist(productId, productName, productPrice, imageUrl);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
// Lightbox Viewer
|
||
|
|
function ensureLightbox() {
|
||
|
|
let lb = document.getElementById('lightbox');
|
||
|
|
if (lb) return lb;
|
||
|
|
lb = document.createElement('div');
|
||
|
|
lb.id = 'lightbox';
|
||
|
|
lb.className = 'lightbox';
|
||
|
|
lb.innerHTML = `
|
||
|
|
<div class="lightbox-content">
|
||
|
|
<button class="lb-nav lb-prev" type="button" aria-label="Previous" onclick="lbPrev(event)"><i class="bi bi-chevron-left"></i></button>
|
||
|
|
<img id="lbImage" alt="@Model.Name" />
|
||
|
|
<button class="lb-nav lb-next" type="button" aria-label="Next" onclick="lbNext(event)"><i class="bi bi-chevron-right"></i></button>
|
||
|
|
<button class="lb-close" type="button" aria-label="Close" onclick="closeLightbox(event)"><i class="bi bi-x-lg"></i></button>
|
||
|
|
</div>`;
|
||
|
|
document.body.appendChild(lb);
|
||
|
|
lb.addEventListener('click', (e) => {
|
||
|
|
if (e.target.id === 'lightbox') closeLightbox(e);
|
||
|
|
});
|
||
|
|
document.addEventListener('keydown', (e) => {
|
||
|
|
if (!lb.classList.contains('open')) return;
|
||
|
|
if (e.key === 'Escape') closeLightbox(e);
|
||
|
|
if (e.key === 'ArrowLeft') lbPrev(e);
|
||
|
|
if (e.key === 'ArrowRight') lbNext(e);
|
||
|
|
});
|
||
|
|
return lb;
|
||
|
|
}
|
||
|
|
|
||
|
|
function openLightbox() {
|
||
|
|
const lb = ensureLightbox();
|
||
|
|
const img = document.getElementById('lbImage');
|
||
|
|
img.src = images[currentIndex] || document.getElementById('galleryImage').src;
|
||
|
|
lb.classList.add('open');
|
||
|
|
document.documentElement.style.overflow = 'hidden';
|
||
|
|
document.body.style.overflow = 'hidden';
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeLightbox(e) {
|
||
|
|
if (e) e.stopPropagation();
|
||
|
|
const lb = document.getElementById('lightbox');
|
||
|
|
if (!lb) return;
|
||
|
|
lb.classList.remove('open');
|
||
|
|
document.documentElement.style.overflow = '';
|
||
|
|
document.body.style.overflow = '';
|
||
|
|
}
|
||
|
|
|
||
|
|
function lbSet(index) {
|
||
|
|
currentIndex = (index + images.length) % images.length;
|
||
|
|
const img = document.getElementById('lbImage');
|
||
|
|
if (img) img.src = images[currentIndex];
|
||
|
|
}
|
||
|
|
function lbPrev(e) { if (e) e.stopPropagation(); lbSet(currentIndex - 1); }
|
||
|
|
function lbNext(e) { if (e) e.stopPropagation(); lbSet(currentIndex + 1); }
|
||
|
|
|
||
|
|
// Variant image switching
|
||
|
|
function selectVariant(element) {
|
||
|
|
// Remove selected class from all swatches
|
||
|
|
document.querySelectorAll('.variant-swatch').forEach(s => s.classList.remove('selected'));
|
||
|
|
|
||
|
|
// Add selected class to clicked swatch
|
||
|
|
element.classList.add('selected');
|
||
|
|
|
||
|
|
// Update ring color on the selected dot
|
||
|
|
const variantData = JSON.parse(element.getAttribute('data-variant'));
|
||
|
|
const dot = element.querySelector('.variant-dot');
|
||
|
|
dot.style.boxShadow = `0 0 0 2px white, 0 0 0 3px ${variantData.colorHex}`;
|
||
|
|
|
||
|
|
// Reset other dots
|
||
|
|
document.querySelectorAll('.variant-swatch:not(.selected) .variant-dot').forEach(d => {
|
||
|
|
d.style.boxShadow = '0 0 0 2px white, 0 0 0 3px #ddd';
|
||
|
|
});
|
||
|
|
|
||
|
|
// Update selected variant name display
|
||
|
|
document.getElementById('selectedVariantName').textContent = variantData.colorName;
|
||
|
|
|
||
|
|
// Store selected variant data for cart/wishlist
|
||
|
|
document.getElementById('selectedVariantData').value = JSON.stringify(variantData);
|
||
|
|
|
||
|
|
// Update gallery to highlight variant images
|
||
|
|
if (variantData.images && variantData.images.length > 0) {
|
||
|
|
console.log('Switching to variant images:', variantData.images);
|
||
|
|
const mainImage = document.getElementById('galleryImage');
|
||
|
|
const gallery = document.querySelector('.gallery');
|
||
|
|
const thumbnails = document.querySelectorAll('.gallery-thumbs .thumb');
|
||
|
|
|
||
|
|
// Enable variant filtering mode
|
||
|
|
gallery.classList.add('variant-filtering');
|
||
|
|
|
||
|
|
// Find the first thumbnail that matches this variant's images
|
||
|
|
let firstMatchIndex = -1;
|
||
|
|
thumbnails.forEach((thumb, index) => {
|
||
|
|
const thumbSrc = thumb.getAttribute('data-src');
|
||
|
|
if (variantData.images.includes(thumbSrc)) {
|
||
|
|
thumb.classList.add('variant-match');
|
||
|
|
if (firstMatchIndex === -1) {
|
||
|
|
firstMatchIndex = index;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
thumb.classList.remove('variant-match');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Switch to the first image of this variant
|
||
|
|
if (firstMatchIndex !== -1 && thumbnails[firstMatchIndex]) {
|
||
|
|
const firstThumb = thumbnails[firstMatchIndex];
|
||
|
|
const firstImgSrc = firstThumb.getAttribute('data-src');
|
||
|
|
|
||
|
|
// Remove all active classes
|
||
|
|
thumbnails.forEach(t => t.classList.remove('active'));
|
||
|
|
firstThumb.classList.add('active');
|
||
|
|
|
||
|
|
// Update main image
|
||
|
|
mainImage.src = firstImgSrc;
|
||
|
|
mainImage.alt = variantData.colorName;
|
||
|
|
|
||
|
|
// Update current index for navigation
|
||
|
|
currentIndex = Array.from(thumbnails).indexOf(firstThumb);
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('Gallery switched to variant, highlighting', variantData.images.length, 'matching images');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update price if there's a price adjustment
|
||
|
|
if (variantData.priceAdjust && variantData.priceAdjust !== 0) {
|
||
|
|
const basePrice = parseFloat('@Model.Price');
|
||
|
|
const adjustedPrice = basePrice + variantData.priceAdjust;
|
||
|
|
const priceElement = document.querySelector('.product-price .price-value');
|
||
|
|
if (priceElement) {
|
||
|
|
priceElement.textContent = `R ${adjustedPrice.toFixed(2)}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Color section toggle (legacy)
|
||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
||
|
|
const colorSection = document.querySelector('.color-section:not(.variant-section)');
|
||
|
|
const colorTrigger = document.getElementById('colorTrigger');
|
||
|
|
|
||
|
|
if (colorTrigger && colorSection) {
|
||
|
|
colorTrigger.addEventListener('click', function(e) {
|
||
|
|
e.preventDefault();
|
||
|
|
colorSection.classList.toggle('show');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Initialize description collapse functionality
|
||
|
|
initializeDescription();
|
||
|
|
});
|
||
|
|
|
||
|
|
function initializeDescription() {
|
||
|
|
const descContent = document.getElementById('descriptionContent');
|
||
|
|
const seeMoreBtn = document.getElementById('seeMoreBtn');
|
||
|
|
|
||
|
|
if (!descContent || !seeMoreBtn) return;
|
||
|
|
|
||
|
|
// Check if content height exceeds 250px
|
||
|
|
if (descContent.scrollHeight > 250) {
|
||
|
|
descContent.classList.add('collapsed');
|
||
|
|
seeMoreBtn.style.display = 'flex';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleDescription() {
|
||
|
|
const descBox = document.getElementById('descriptionBox');
|
||
|
|
const chevron = document.getElementById('descChevron');
|
||
|
|
|
||
|
|
descBox.classList.toggle('closed');
|
||
|
|
|
||
|
|
if (descBox.classList.contains('closed')) {
|
||
|
|
chevron.style.transform = 'rotate(-90deg)';
|
||
|
|
} else {
|
||
|
|
chevron.style.transform = 'rotate(0deg)';
|
||
|
|
// Re-initialize collapsed state when opening
|
||
|
|
initializeDescription();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function expandDescription() {
|
||
|
|
const descContent = document.getElementById('descriptionContent');
|
||
|
|
const seeMoreBtn = document.getElementById('seeMoreBtn');
|
||
|
|
|
||
|
|
if (descContent.classList.contains('collapsed')) {
|
||
|
|
descContent.classList.remove('collapsed');
|
||
|
|
seeMoreBtn.innerHTML = '<i class="bi bi-chevron-up"></i> See Less';
|
||
|
|
} else {
|
||
|
|
descContent.classList.add('collapsed');
|
||
|
|
seeMoreBtn.innerHTML = '<i class="bi bi-chevron-down"></i> See More';
|
||
|
|
// Scroll back to description tab
|
||
|
|
document.querySelector('.description-tab').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
}
|