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:
Local Server
2025-12-13 22:34:11 -06:00
parent 8bb6430a70
commit 703ab57984
253 changed files with 29870 additions and 157 deletions

715
Views/Shop/Details.cshtml Executable file
View 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
View 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>