Fix admin route access and backend configuration
- Added /admin redirect to login page in nginx config - Fixed backend server.js route ordering for proper admin handling - Updated authentication middleware and routes - Added user management routes - Configured PostgreSQL integration - Updated environment configuration
This commit is contained in:
715
Views/Shop/Details.cshtml
Executable file
715
Views/Shop/Details.cshtml
Executable file
@@ -0,0 +1,715 @@
|
||||
@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>
|
||||
}
|
||||
130
Views/Shop/Index.cshtml
Executable file
130
Views/Shop/Index.cshtml
Executable file
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="light">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<meta name="description" content="@ViewData["MetaDescription"] ?? " Sky Art Shop - Scrapbooking, journaling,
|
||||
cardmaking, and collaging stationery."" />
|
||||
<title>SkyArt - @ViewData["Title"]</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="~/assets/css/main.css?v=@DateTime.Now.Ticks" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar">
|
||||
<div class="navbar-content">
|
||||
<div class="nav-brand">
|
||||
<a href="/">
|
||||
<img src="/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg" alt="Logo" class="logo-image" />
|
||||
<h1>@(ViewBag.SiteSettings?.SiteName ?? "Sky Art Shop")</h1>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-center">
|
||||
@await Component.InvokeAsync("Navigation", new { location = "navbar" })
|
||||
</div>
|
||||
|
||||
<div class="nav-icons">
|
||||
<div class="dropdown-container">
|
||||
<a href="#" class="nav-icon" id="wishlistBtn" aria-label="Wishlist">
|
||||
<i class="bi bi-heart"></i>
|
||||
<span class="badge">0</span>
|
||||
</a>
|
||||
<div class="icon-dropdown" id="wishlistDropdown">
|
||||
<div class="dropdown-header">
|
||||
<h4>My Wishlist</h4>
|
||||
</div>
|
||||
<div class="dropdown-items" id="wishlistItems">
|
||||
<p class="empty-message">Your wishlist is empty</p>
|
||||
</div>
|
||||
<div class="dropdown-footer">
|
||||
<a href="/shop" class="btn-view-all">Continue Shopping</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown-container">
|
||||
<a href="#" class="nav-icon" id="cartBtn" aria-label="Cart">
|
||||
<i class="bi bi-cart"></i>
|
||||
<span class="badge">0</span>
|
||||
</a>
|
||||
<div class="icon-dropdown" id="cartDropdown">
|
||||
<div class="dropdown-header">
|
||||
<h4>Shopping Cart</h4>
|
||||
</div>
|
||||
<div class="dropdown-items" id="cartItems">
|
||||
<p class="empty-message">Your cart is empty</p>
|
||||
</div>
|
||||
<div class="dropdown-footer">
|
||||
<div class="dropdown-total">
|
||||
<span>Total:</span>
|
||||
<span id="cartTotal">$0.00</span>
|
||||
</div>
|
||||
<a href="/checkout" class="btn-checkout">Checkout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="nav-toggle" aria-label="Menu" aria-expanded="false">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-dropdown" id="navDropdown">
|
||||
@await Component.InvokeAsync("Navigation", new { location = "dropdown" })
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@RenderBody()
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-brand">
|
||||
<h2>@(ViewBag.SiteSettings?.SiteName ?? "Sky Art Shop")</h2>
|
||||
<p>Follow Us</p>
|
||||
<div class="social-links">
|
||||
<a href="#instagram" aria-label="Instagram">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<h3>Additional Links</h3>
|
||||
@await Component.InvokeAsync("FooterPages")
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>@(ViewBag.SiteSettings?.FooterText ?? "© 2035 by Sky Art Shop. All rights reserved.")</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Force light mode on page load
|
||||
(function() {
|
||||
document.documentElement.setAttribute('data-bs-theme', 'light');
|
||||
document.documentElement.style.colorScheme = 'light';
|
||||
document.body.setAttribute('data-bs-theme', 'light');
|
||||
document.body.style.colorScheme = 'light';
|
||||
})();
|
||||
</script>
|
||||
<script src="~/assets/js/main.js?v=@DateTime.Now.Ticks"></script>
|
||||
<script src="~/assets/js/cart.js?v=@DateTime.Now.Ticks"></script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user