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

View File

@@ -0,0 +1,81 @@
@model List<SkyArtShop.Models.Product>
@{
ViewData["Title"] = "Shop";
var categories = ViewBag.Categories as List<string> ?? new();
var selected = ViewBag.SelectedCategory as string;
}
<section class="shop-hero">
<div class="container">
<h1>Shop All Products</h1>
<p class="hero-subtitle">Find everything you need for your creative projects</p>
</div>
</section>
<section class="shop-filters">
<div class="container">
<div class="filter-bar">
<div class="filter-group">
<label for="category-filter">Category:</label>
<select id="category-filter" onchange="window.location.href='/shop?category='+this.value;">
<option value="">All Products</option>
@foreach (var cat in categories)
{
<option value="@cat" selected="@(selected == cat ? "selected" : null)">@cat</option>
}
</select>
</div>
</div>
</div>
</section>
<section class="shop-products">
<div class="container">
<div class="products-grid">
@foreach (var product in Model)
{
<div class="product-card">
<a href="/shop/product/@product.Id" class="product-link">
<div class="product-image">
@{
var displayImage = !string.IsNullOrEmpty(product.ImageUrl)
? product.ImageUrl
: (product.Images != null && product.Images.Count > 0
? product.Images[0]
: "/assets/images/placeholder.jpg");
}
<img src="@displayImage" alt="@product.Name" loading="lazy" />
</div>
<h3>@product.Name</h3>
@if (!string.IsNullOrEmpty(product.Color))
{
<span class="product-color-badge">@product.Color</span>
}
<div class="product-description">@Html.Raw(product.ShortDescription ?? product.Description)</div>
<p class="price">$@product.Price.ToString("F2")</p>
</a>
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
<button class="btn btn-small btn-icon"
onclick="addToWishlist('@product.Id', '@product.Name', @product.Price, '@(product.Images != null && product.Images.Count > 0 ? product.Images[0] : product.ImageUrl ?? "/assets/images/placeholder.jpg")')"
aria-label="Add to wishlist">
<i class="bi bi-heart"></i>
</button>
<button class="btn btn-small btn-icon"
onclick="addToCart('@product.Id', '@product.Name', @product.Price, '@(product.Images != null && product.Images.Count > 0 ? product.Images[0] : product.ImageUrl ?? "/assets/images/placeholder.jpg")')" aria-label="Add to cart">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path
d="M7 4h-2l-1 2h-2v2h2l3.6 7.59-1.35 2.44c-.16.28-.25.61-.25.97 0 1.1.9 2 2 2h12v-2h-11.1c-.14 0-.25-.11-.25-.25l.03-.12.9-1.63h7.42c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.08-.14.12-.31.12-.48 0-.55-.45-1-1-1h-14.31l-.94-2zm3 17c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm8 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
</svg>
</button>
</div>
</div>
}
</div>
</div>
</section>
@section Scripts {
<script>
// Cart functionality now loaded from cart.js
</script>
}

View File

@@ -0,0 +1,463 @@
@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>
<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 picker after actions (still in info pane) -->
@{
var hasColors = (Model.Colors != null && Model.Colors.Any()) || !string.IsNullOrEmpty(Model.Color);
List<string> selectedColors = new List<string>();
Dictionary<string, string> colorHexMap = new Dictionary<string, string>();
if (hasColors)
{
selectedColors = Model.Colors != null && Model.Colors.Any()
? Model.Colors
: new List<string> { Model.Color ?? "" };
colorHexMap = new Dictionary<string, string> {
{"Red", "#FF0000"}, {"Blue", "#0000FF"}, {"Green", "#00FF00"}, {"Yellow", "#FFFF00"},
{"Orange", "#FFA500"}, {"Purple", "#800080"}, {"Pink", "#FFC0CB"}, {"Black", "#000000"},
{"White", "#FFFFFF"}, {"Gray", "#808080"}, {"Brown", "#A52A2A"}, {"Gold", "#FFD700"},
{"Silver", "#C0C0C0"}, {"Multicolor", "linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet)"},
{"Burgundy", "#800020"}, {"Rust Orange", "#B7410E"}, {"Teal", "#008080"},
{"Lime Green", "#32CD32"}, {"Navy Blue", "#000080"}, {"Royal Blue", "#4169E1"},
{"Dark Green", "#006400"}, {"Hunter Green", "#355E3B"}
};
}
}
@if (hasColors)
{
<div class="color-section">
<div class="color-row" id="colorTrigger">
<span class="label">Available Colors:</span>
<span class="value">@string.Join(", ", selectedColors)</span>
<i class="bi bi-chevron-down color-arrow"></i>
</div>
<div class="swatches" id="colorSwatches">
@foreach (var colorName in selectedColors)
{
var hexColor = colorHexMap.ContainsKey(colorName) ? colorHexMap[colorName] : "#808080";
var isGradient = colorName == "Multicolor";
var bgStyle = isGradient ? $"background: {hexColor};" : $"background-color: {hexColor};";
<div class="swatch active">
<span class="dot" style="@bgStyle"></span>
<span class="name">@colorName</span>
</div>
}
</div>
</div>
}
@if (!string.IsNullOrEmpty(Model.ShortDescription))
{
<div class="short">
@Model.ShortDescription
</div>
}
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(Model.Description))
{
<div class="row mt-4">
<div class="col-12">
<div class="desc-block">
<h3>Description</h3>
<div class="content">
@Html.Raw(Model.Description)
</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);
}
function slideImage(direction) {
currentIndex = (currentIndex + direction + images.length) % images.length;
const nextSrc = images[currentIndex];
changeImage(nextSrc, direction);
// 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';
const productName = '@Model.Name';
const productPrice = @Model.Price;
const imageUrl = '@(Model.Images != null && Model.Images.Count > 0 ? Model.Images[0] : "/assets/images/placeholder.jpg")';
// Call the cart function multiple times for quantity
for (let i = 0; i < quantity; i++) {
addToCart(productId, productName, productPrice, imageUrl);
}
// Show success message
alert(`Added ${quantity} x ${productName} to cart!`);
}
function addToWishlistFromDetail() {
const productId = '@Model.Id';
const productName = '@Model.Name';
const productPrice = @Model.Price;
const imageUrl = '@(Model.Images != null && Model.Images.Count > 0 ? Model.Images[0] : "/assets/images/placeholder.jpg")';
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); }
// Color section toggle
document.addEventListener('DOMContentLoaded', function() {
const colorSection = document.querySelector('.color-section');
const colorTrigger = document.getElementById('colorTrigger');
if (colorTrigger && colorSection) {
colorTrigger.addEventListener('click', function(e) {
e.preventDefault();
colorSection.classList.toggle('show');
});
}
});
</script>
}