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,45 @@
@{
Layout = "~/Views/Shared/_AdminLayout.cshtml";
ViewData["Title"] = "Change Password";
}
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Change Your Password</h5>
</div>
<div class="card-body">
@if (ViewBag.Error != null)
{
<div class="alert alert-danger">@ViewBag.Error</div>
}
<form method="post" action="/admin/change-password">
<div class="mb-3">
<label for="currentPassword" class="form-label">Current Password</label>
<input type="password" class="form-control" id="currentPassword" name="currentPassword" required>
</div>
<div class="mb-3">
<label for="newPassword" class="form-label">New Password</label>
<input type="password" class="form-control" id="newPassword" name="newPassword" required minlength="6">
<small class="form-text text-muted">At least 6 characters</small>
</div>
<div class="mb-3">
<label for="confirmPassword" class="form-label">Confirm New Password</label>
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword" required>
</div>
<div class="d-flex justify-content-between">
<a href="/admin/dashboard" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Change Password</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>

189
Views/Admin/Dashboard.cshtml Executable file
View File

@@ -0,0 +1,189 @@
@{
ViewData["Title"] = "Dashboard";
Layout = "_AdminLayout";
}
<div class="row">
<div class="col-md-3">
<a href="/admin/products" class="text-decoration-none">
<div class="card dashboard-stat-card">
<div class="card-body">
<h6 class="text-muted">Total Products</h6>
<h2 class="mb-0">@ViewBag.ProductCount</h2>
<span class="stat-link">Manage →</span>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/portfolio/projects" class="text-decoration-none">
<div class="card dashboard-stat-card">
<div class="card-body">
<h6 class="text-muted">Portfolio Projects</h6>
<h2 class="mb-0">@ViewBag.ProjectCount</h2>
<span class="stat-link">Manage →</span>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/blog" class="text-decoration-none">
<div class="card dashboard-stat-card">
<div class="card-body">
<h6 class="text-muted">Blog Posts</h6>
<h2 class="mb-0">@ViewBag.BlogCount</h2>
<span class="stat-link">Manage →</span>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/pages" class="text-decoration-none">
<div class="card dashboard-stat-card">
<div class="card-body">
<h6 class="text-muted">Custom Pages</h6>
<h2 class="mb-0">@ViewBag.PageCount</h2>
<span class="stat-link">Manage →</span>
</div>
</div>
</a>
</div>
</div>
<div class="row mt-4">
<div class="col-md-3">
<a href="/admin/homepage" class="text-decoration-none">
<div class="card dashboard-stat-card">
<div class="card-body text-center">
<i class="bi bi-house-fill" style="font-size: 2.5rem; color: #28a745;"></i>
<h6 class="mt-3 mb-0">Homepage Editor</h6>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/products/create" class="text-decoration-none">
<div class="card dashboard-stat-card">
<div class="card-body text-center">
<i class="bi bi-plus-circle" style="font-size: 2.5rem; color: #3498db;"></i>
<h6 class="mt-3 mb-0">Add New Product</h6>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/blog/create" class="text-decoration-none">
<div class="card dashboard-stat-card">
<div class="card-body text-center">
<i class="bi bi-plus-circle" style="font-size: 2.5rem; color: #3498db;"></i>
<h6 class="mt-3 mb-0">Create Blog Post</h6>
</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/portfolio/projects/create" class="text-decoration-none">
<div class="card dashboard-stat-card">
<div class="card-body text-center">
<i class="bi bi-plus-circle" style="font-size: 2.5rem; color: #3498db;"></i>
<h6 class="mt-3 mb-0">Add Portfolio Project</h6>
</div>
</div>
</a>
</div>
</div>
<div class="row mt-5">
<div class="col-md-4">
<div class="card system-info-card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-info-circle-fill"></i> System Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<strong><i class="bi bi-globe"></i> Site Name:</strong><br>
<span class="text-muted">@ViewBag.SiteName</span>
</div>
<div class="mb-3">
<strong><i class="bi bi-database-fill"></i> Database:</strong><br>
<span id="dbStatus" class="badge bg-secondary">
<span class="spinner-border spinner-border-sm"></span> Checking...
</span>
<small id="dbInfo" class="d-block text-muted mt-1"></small>
</div>
<div class="mb-3">
<strong><i class="bi bi-person-circle"></i> Admin User:</strong><br>
<span class="text-muted">@ViewBag.AdminEmail</span>
</div>
<div class="mb-3">
<strong><i class="bi bi-clock-fill"></i> Server Time:</strong><br>
<span id="serverTime" class="text-muted">@DateTime.Now.ToString("MMM dd, yyyy HH:mm:ss")</span>
</div>
<div>
<strong><i class="bi bi-hdd-fill"></i> System:</strong><br>
<span id="systemStatus" class="badge bg-success">
<i class="bi bi-check-circle-fill"></i> Online
</span>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
// Update server time every second
function updateServerTime() {
const timeEl = document.getElementById('serverTime');
if (timeEl) {
const now = new Date();
timeEl.textContent = now.toLocaleString('en-US', {
month: 'short',
day: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
}
}
// Check database connection status
async function checkDatabaseStatus() {
const statusEl = document.getElementById('dbStatus');
const infoEl = document.getElementById('dbInfo');
try {
const response = await fetch('/admin/system-status');
const data = await response.json();
if (data.databaseConnected) {
statusEl.className = 'badge bg-success';
statusEl.innerHTML = '<i class="bi bi-check-circle-fill"></i> PostgreSQL Connected';
infoEl.innerHTML = `Host: ${data.dbHost || 'localhost'} | Database: ${data.dbName || 'skyartshop'}`;
} else {
statusEl.className = 'badge bg-danger';
statusEl.innerHTML = '<i class="bi bi-x-circle-fill"></i> Database Disconnected';
infoEl.textContent = data.error || 'Connection failed';
}
} catch (error) {
statusEl.className = 'badge bg-warning';
statusEl.innerHTML = '<i class="bi bi-exclamation-triangle-fill"></i> Status Unknown';
infoEl.textContent = 'Unable to check status';
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
updateServerTime();
checkDatabaseStatus();
// Update time every second
setInterval(updateServerTime, 1000);
// Check database status every 10 seconds
setInterval(checkDatabaseStatus, 10000);
});
</script>
}

430
Views/Admin/Login.cshtml Executable file
View File

@@ -0,0 +1,430 @@
<!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">
<title>SkyArt - @ViewData["Title"]</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" data-theme="light">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
color-scheme: light !important;
}
html {
color-scheme: light !important;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0;
padding: 0;
overflow-x: hidden;
background-color: #f8f9fa !important;
color: #212529 !important;
color-scheme: light !important;
}
/* Force all Bootstrap components to light mode */
* {
color-scheme: light !important;
}
.main-content {
background-color: #ffffff !important;
color: #212529 !important;
}
.card {
background-color: #ffffff !important;
color: #212529 !important;
}
.table {
background-color: #ffffff !important;
color: #212529 !important;
}
input, textarea, select {
background-color: #ffffff !important;
color: #212529 !important;
}
.sidebar {
height: 100vh;
background: linear-gradient(180deg, #2c3e50 0%, #1a252f 100%);
color: white;
position: fixed;
top: 0;
left: 0;
width: 250px;
overflow-y: auto;
overflow-x: hidden;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar::-webkit-scrollbar {
width: 6px;
}
.sidebar::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 10px;
margin: 10px 0;
}
.sidebar::-webkit-scrollbar-thumb {
background: rgba(52, 152, 219, 0.6);
border-radius: 10px;
transition: background 0.3s;
}
.sidebar::-webkit-scrollbar-thumb:hover {
background: rgba(52, 152, 219, 1);
}
/* Firefox scrollbar */
.sidebar {
scrollbar-width: thin;
scrollbar-color: rgba(52, 152, 219, 0.6) rgba(0, 0, 0, 0.2);
}
.sidebar .brand {
padding: 20px;
font-size: 1.5rem;
font-weight: bold;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
position: sticky;
top: 0;
background: linear-gradient(180deg, #2c3e50 0%, #243442 100%);
z-index: 10;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.sidebar nav {
padding: 10px 0 30px 0;
}
.sidebar hr {
opacity: 0.2;
}
.sidebar .section-label {
padding: 15px 20px 5px 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: rgba(255, 255, 255, 0.5);
margin-top: 10px;
}
.sidebar .nav-link {
color: rgba(255, 255, 255, 0.85);
padding: 12px 20px;
display: flex;
align-items: center;
transition: all 0.3s ease;
border-left: 3px solid transparent;
text-decoration: none;
position: relative;
overflow: hidden;
}
.sidebar .nav-link:hover {
background: rgba(52, 152, 219, 0.15);
color: white;
border-left-color: #3498db;
padding-left: 25px;
}
.sidebar .nav-link.active {
background: rgba(52, 152, 219, 0.25);
color: white;
border-left-color: #3498db;
font-weight: 600;
}
.sidebar .nav-link.active::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: #3498db;
box-shadow: 0 0 10px #3498db;
}
.sidebar .nav-link i {
margin-right: 12px;
width: 20px;
font-size: 1.1rem;
transition: transform 0.3s ease;
}
.sidebar .nav-link:hover i {
transform: scale(1.2);
}
.main-content {
margin-left: 250px;
padding: 20px;
background: #f8f9fa;
min-height: 100vh;
}
.top-bar {
background: white;
padding: 15px 30px;
margin: -20px -20px 20px -20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.card {
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.dashboard-stat-card {
transition: all 0.3s ease;
cursor: pointer;
border-left: 4px solid transparent;
}
.dashboard-stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
border-left-color: #3498db;
}
.dashboard-stat-card h6 {
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.dashboard-stat-card h2 {
color: #2c3e50;
font-weight: 700;
font-size: 2.5rem;
margin: 10px 0;
}
.stat-link {
color: #3498db;
font-size: 0.875rem;
font-weight: 600;
display: inline-block;
margin-top: 10px;
}
.dashboard-stat-card:hover .stat-link {
text-decoration: underline;
}
.system-info-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
}
.system-info-card .card-header {
background: transparent;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
color: white;
}
.system-info-card .card-body p {
color: rgba(255, 255, 255, 0.95);
margin-bottom: 10px;
}
.btn-group-sm .btn {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.alert {
border-radius: 8px;
}
/* Scroll indicator */
.scroll-indicator {
position: sticky;
bottom: 0;
left: 0;
right: 0;
height: 40px;
background: linear-gradient(to top, #1a252f 0%, transparent 100%);
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.3s;
}
.scroll-indicator.hidden {
opacity: 0;
}
.scroll-indicator i {
color: rgba(52, 152, 219, 0.8);
font-size: 1.5rem;
animation: bounce 2s infinite;
}
@@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
60% {
transform: translateY(-5px);
}
}
/* Mobile responsive */
@@media (max-width: 768px) {
.sidebar {
width: 100%;
height: auto;
position: relative;
}
.main-content {
margin-left: 0;
}
}
</style>
</head>
<body>
<div class="sidebar">
<div class="brand">
<i class="bi bi-shop"></i> Sky Art Shop
</div>
<nav class="nav flex-column">
<a class="nav-link @(ViewContext.RouteData.Values["Action"]?.ToString() == "Dashboard" ? "active" : "")"
href="/admin/dashboard">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
<div class="section-label">
<i class="bi bi-folder"></i> CONTENT
</div>
<a class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "AdminPages" ? "active" : "")"
href="/admin/pages">
<i class="bi bi-file-earmark-text"></i> Pages
</a>
<a class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "AdminBlog" ? "active" : "")"
href="/admin/blog">
<i class="bi bi-journal-text"></i> Blog
</a>
<a class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "AdminPortfolio" ? "active" : "")"
href="/admin/portfolio/categories">
<i class="bi bi-images"></i> Portfolio
</a>
<a class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "AdminProducts" ? "active" : "")"
href="/admin/products">
<i class="bi bi-cart"></i> Products
</a>
<div class="section-label">
<i class="bi bi-gear"></i> SETTINGS
</div>
<a class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "AdminUsers" ? "active" : "")"
href="/admin/users">
<i class="bi bi-people"></i> User Management
</a>
<a class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "AdminHomepage" ? "active" : "")"
href="/admin/homepage">
<i class="bi bi-house-fill"></i> Homepage Editor
</a>
<a class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "AdminMenu" ? "active" : "")"
href="/admin/menu">
<i class="bi bi-list"></i> Navigation Menu
</a>
<a class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "AdminSettings" ? "active" : "")"
href="/admin/settings">
<i class="bi bi-gear"></i> Site Settings
</a>
<a class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "AdminUpload" ? "active" : "")"
href="/admin/upload">
<i class="bi bi-cloud-upload"></i> Media Upload
</a>
<div class="section-label">
<i class="bi bi-layout-sidebar"></i> SYSTEM
</div>
<a class="nav-link" href="/" target="_blank">
<i class="bi bi-box-arrow-up-right"></i> View Site
</a>
<a class="nav-link" href="/admin/logout">
<i class="bi bi-box-arrow-right"></i> Logout
</a>
</nav>
<div class="scroll-indicator" id="scrollIndicator">
<i class="bi bi-chevron-down"></i>
</div>
</div>
<div class="main-content">
<div class="top-bar">
<h4 class="mb-0">@ViewData["Title"]</h4>
<div>
<span class="text-muted">Welcome, Admin</span>
</div>
</div>
<partial name="_AdminAlerts" />
@RenderBody()
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.ckeditor.com/ckeditor5/40.1.0/classic/ckeditor.js"></script>
<script src="~/assets/js/admin.js"></script>
<script>
// Sidebar scroll indicator
document.addEventListener('DOMContentLoaded', function() {
const sidebar = document.querySelector('.sidebar');
const scrollIndicator = document.getElementById('scrollIndicator');
if (sidebar && scrollIndicator) {
function updateScrollIndicator() {
const isAtBottom = sidebar.scrollHeight - sidebar.scrollTop <= sidebar.clientHeight + 10;
if (isAtBottom) {
scrollIndicator.classList.add('hidden');
} else {
scrollIndicator.classList.remove('hidden');
}
}
// Check on scroll
sidebar.addEventListener('scroll', updateScrollIndicator);
// Initial check
updateScrollIndicator();
// Check after window resize
window.addEventListener('resize', updateScrollIndicator);
}
});
</script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

430
Views/Admin/Settings.cshtml Executable file
View File

@@ -0,0 +1,430 @@
<!-- Product Variant Manager Modal -->
<div class="modal fade" id="variantManagerModal" tabindex="-1" aria-labelledby="variantManagerLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="variantManagerLabel">
<i class="bi bi-palette"></i> Manage Color Variants
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> <strong>How it works:</strong> Create color variants and assign specific images to each color. Customers will see the assigned images when they select a color on the product page.
</div>
<!-- Variant List -->
<div id="variantList" class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Color Variants (<span id="variantCount">0</span>)</h6>
<button type="button" class="btn btn-sm btn-success" onclick="addNewVariant()">
<i class="bi bi-plus-circle"></i> Add Color Variant
</button>
</div>
<div id="variantsContainer">
<!-- Variants will be added here -->
</div>
</div>
<!-- Add/Edit Variant Form -->
<div id="variantForm" style="display: none;" class="border rounded p-3 bg-light">
<h6 class="mb-3">
<span id="formTitle">Add New Variant</span>
<button type="button" class="btn btn-sm btn-outline-secondary float-end" onclick="cancelVariantForm()">
<i class="bi bi-x"></i> Cancel
</button>
</h6>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Color Name *</label>
<input type="text" id="variantColorName" class="form-control" placeholder="e.g., Ocean Blue, Cherry Red">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Color Hex Code *</label>
<div class="input-group">
<input type="color" id="variantColorPicker" class="form-control form-control-color" value="#3498db">
<input type="text" id="variantColorHex" class="form-control" value="#3498db" pattern="^#[0-9A-Fa-f]{6}$">
</div>
<small class="text-muted">Click the color box to open the color picker</small>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Stock Quantity</label>
<input type="number" id="variantStock" class="form-control" value="0" min="0">
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Price Adjustment</label>
<input type="number" step="0.01" id="variantPriceAdjust" class="form-control" placeholder="0.00">
<small class="text-muted">Optional: +/- from base price</small>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Variant SKU</label>
<input type="text" id="variantSKU" class="form-control" placeholder="Optional">
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Assign Images to This Color *</label>
<div class="d-flex gap-2 align-items-center mb-2">
<button type="button" class="btn btn-sm btn-primary" onclick="selectVariantImages()">
<i class="bi bi-images"></i> Select Images
</button>
<span class="text-muted" id="variantImageCount">No images selected</span>
</div>
<div id="variantImagePreview" class="d-flex flex-wrap gap-2">
<!-- Selected images will appear here -->
</div>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="variantAvailable" checked>
<label class="form-check-label" for="variantAvailable">
Available for purchase
</label>
</div>
<div class="d-grid">
<button type="button" class="btn btn-success" onclick="saveVariant()">
<i class="bi bi-check-circle"></i> <span id="saveButtonText">Add Variant</span>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="applyVariants()">
<i class="bi bi-save"></i> Apply Changes
</button>
</div>
</div>
</div>
</div>
<style>
.variant-card {
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
transition: all 0.3s;
background: white;
}
.variant-card:hover {
border-color: #3498db;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.variant-color-swatch {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid #ddd;
display: inline-block;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.variant-image-thumb {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
border: 1px solid #ddd;
}
.predefined-color-btn {
width: 35px;
height: 35px;
border-radius: 50%;
border: 2px solid #ddd;
cursor: pointer;
transition: transform 0.2s;
display: inline-block;
margin: 3px;
}
.predefined-color-btn:hover {
transform: scale(1.15);
border-color: #3498db;
}
.predefined-color-btn.selected {
border: 3px solid #3498db;
box-shadow: 0 0 10px rgba(52, 152, 219, 0.5);
}
</style>
<script>
let productVariants = [];
let currentEditIndex = -1;
let currentVariantImages = [];
let availableProductImages = [];
// Initialize variant manager with existing data
function initVariantManager(existingVariants, productImages) {
productVariants = existingVariants || [];
availableProductImages = productImages || [];
// Store product images globally for image picker
window.currentProductImages = productImages || [];
renderVariantList();
}
// Render variant list
function renderVariantList() {
const container = document.getElementById('variantsContainer');
document.getElementById('variantCount').textContent = productVariants.length;
if (productVariants.length === 0) {
container.innerHTML = '<p class="text-muted text-center py-3">No color variants added yet. Click "Add Color Variant" to get started.</p>';
return;
}
container.innerHTML = productVariants.map((variant, index) => {
const colorHex = variant.ColorHex || variant.colorHex || '#cccccc';
const colorName = variant.ColorName || variant.colorName || 'Unknown';
const images = variant.Images || variant.images || [];
const stockQty = variant.StockQuantity ?? variant.stockQuantity ?? 0;
const isAvailable = variant.IsAvailable ?? variant.isAvailable ?? true;
return `
<div class="variant-card">
<div class="row align-items-center">
<div class="col-auto">
<div class="variant-color-swatch" style="background-color: ${colorHex};" title="${colorName}"></div>
</div>
<div class="col">
<h6 class="mb-1">${colorName}</h6>
<small class="text-muted">
${images.length} images |
Stock: ${stockQty} |
${isAvailable ? '<span class="badge bg-success">Available</span>' : '<span class="badge bg-danger">Unavailable</span>'}
</small>
</div>
<div class="col-auto">
<div class="d-flex flex-wrap gap-1">
${images.slice(0, 3).map(img =>
`<img src="${img}" class="variant-image-thumb" alt="${colorName}">`
).join('')}
${images.length > 3 ? `<span class="badge bg-secondary align-self-center">+${images.length - 3}</span>` : ''}
</div>
</div>
<div class="col-auto">
<button class="btn btn-sm btn-outline-primary" onclick="editVariant(${index})">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteVariant(${index})">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
`;
}).join('');
}
// Add new variant
function addNewVariant() {
currentEditIndex = -1;
currentVariantImages = [];
document.getElementById('formTitle').textContent = 'Add New Variant';
document.getElementById('saveButtonText').textContent = 'Add Variant';
// Reset form
document.getElementById('variantColorName').value = '';
document.getElementById('variantColorPicker').value = '#3498db';
document.getElementById('variantColorHex').value = '#3498db';
document.getElementById('variantStock').value = '0';
document.getElementById('variantPriceAdjust').value = '';
document.getElementById('variantSKU').value = '';
document.getElementById('variantAvailable').checked = true;
document.getElementById('variantImagePreview').innerHTML = '';
document.getElementById('variantImageCount').textContent = 'No images selected';
document.getElementById('variantForm').style.display = 'block';
}
// Edit variant
function editVariant(index) {
currentEditIndex = index;
const variant = productVariants[index];
// Handle both uppercase and lowercase property names
const colorName = variant.ColorName || variant.colorName || '';
const colorHex = variant.ColorHex || variant.colorHex || '#3498db';
const images = variant.Images || variant.images || [];
const stockQty = variant.StockQuantity ?? variant.stockQuantity ?? 0;
const priceAdj = variant.PriceAdjustment ?? variant.priceAdjustment ?? null;
const sku = variant.SKU || variant.sku || '';
const isAvailable = variant.IsAvailable ?? variant.isAvailable ?? true;
currentVariantImages = [...images];
document.getElementById('formTitle').textContent = 'Edit Variant';
document.getElementById('saveButtonText').textContent = 'Update Variant';
document.getElementById('variantColorName').value = colorName;
document.getElementById('variantColorPicker').value = colorHex;
document.getElementById('variantColorHex').value = colorHex;
document.getElementById('variantStock').value = stockQty;
document.getElementById('variantPriceAdjust').value = priceAdj || '';
document.getElementById('variantSKU').value = sku;
document.getElementById('variantAvailable').checked = isAvailable;
updateVariantImagePreview();
document.getElementById('variantForm').style.display = 'block';
console.log('[Edit Variant] Loaded variant:', { colorName, colorHex, imageCount: images.length });
}
// Delete variant
function deleteVariant(index) {
if (confirm('Are you sure you want to delete this color variant?')) {
productVariants.splice(index, 1);
renderVariantList();
}
}
// Cancel form
function cancelVariantForm() {
document.getElementById('variantForm').style.display = 'none';
currentEditIndex = -1;
currentVariantImages = [];
}
// Save variant
function saveVariant() {
const colorName = document.getElementById('variantColorName').value.trim();
const colorHex = document.getElementById('variantColorHex').value.trim();
if (!colorName || !colorHex) {
alert('Please enter a color name and select a color.');
return;
}
if (currentVariantImages.length === 0) {
alert('Please assign at least one image to this color variant.');
return;
}
const variant = {
ColorName: colorName,
ColorHex: colorHex,
Images: [...currentVariantImages],
StockQuantity: parseInt(document.getElementById('variantStock').value) || 0,
PriceAdjustment: parseFloat(document.getElementById('variantPriceAdjust').value) || null,
SKU: document.getElementById('variantSKU').value.trim() || '',
IsAvailable: document.getElementById('variantAvailable').checked
};
if (currentEditIndex >= 0) {
productVariants[currentEditIndex] = variant;
} else {
productVariants.push(variant);
}
renderVariantList();
cancelVariantForm();
}
// Select images for variant
function selectVariantImages() {
console.log('selectVariantImages called');
// Check if openImagePicker function exists
if (typeof openImagePicker !== 'function') {
console.error('openImagePicker function not found!');
alert('Error: Image picker function not available. Please ensure the page has fully loaded.');
return;
}
console.log('Calling openImagePicker...');
openImagePicker(function(selectedUrls) {
console.log('Image picker callback received:', selectedUrls);
currentVariantImages = selectedUrls;
updateVariantImagePreview();
}, 'multiple');
}
// Update variant image preview
function updateVariantImagePreview() {
const preview = document.getElementById('variantImagePreview');
const count = document.getElementById('variantImageCount');
if (currentVariantImages.length === 0) {
preview.innerHTML = '';
count.textContent = 'No images selected';
return;
}
count.textContent = `${currentVariantImages.length} image(s) selected`;
preview.innerHTML = currentVariantImages.map((img, idx) => `
<div class="position-relative">
<img src="${img}" class="variant-image-thumb" alt="Variant image">
<button type="button" class="btn btn-danger btn-sm position-absolute top-0 end-0"
style="padding: 2px 6px; font-size: 0.7rem;"
onclick="removeVariantImage(${idx})">
<i class="bi bi-x"></i>
</button>
</div>
`).join('');
}
// Remove variant image
function removeVariantImage(index) {
currentVariantImages.splice(index, 1);
updateVariantImagePreview();
}
// Sync color picker and hex input
document.addEventListener('DOMContentLoaded', function() {
const colorPicker = document.getElementById('variantColorPicker');
const colorHex = document.getElementById('variantColorHex');
if (colorPicker && colorHex) {
colorPicker.addEventListener('input', function() {
colorHex.value = this.value;
});
colorHex.addEventListener('input', function() {
if (/^#[0-9A-Fa-f]{6}$/.test(this.value)) {
colorPicker.value = this.value;
}
});
}
});
// Apply variants (to be called when modal is closed)
function applyVariants() {
// Store variants in hidden field
document.getElementById('productVariantsData').value = JSON.stringify(productVariants);
bootstrap.Modal.getInstance(document.getElementById('variantManagerModal')).hide();
// Update UI to show variant count
updateVariantSummary();
}
// Update variant summary on main form
function updateVariantSummary() {
const summary = document.getElementById('variantSummary');
if (summary) {
summary.innerHTML = productVariants.length > 0
? `<span class="badge bg-success">${productVariants.length} color variants configured</span>`
: '<span class="badge bg-secondary">No variants</span>';
}
}
</script>

75
Views/Admin/Test.cshtml Executable file
View File

@@ -0,0 +1,75 @@
@{
ViewData["Title"] = "Backend Diagnostic Test";
Layout = "_AdminLayout";
}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h2>Backend Navigation Diagnostic Test</h2>
<p>Click the links below to test navigation:</p>
<div class="card mt-4">
<div class="card-header bg-primary text-white">
<h4>Test Links</h4>
</div>
<div class="card-body">
<div class="list-group">
<a href="/admin/pages" class="list-group-item list-group-item-action">
<i class="bi bi-file-earmark-text"></i> Pages
</a>
<a href="/admin/products" class="list-group-item list-group-item-action">
<i class="bi bi-cart"></i> Products
</a>
<a href="/admin/blog" class="list-group-item list-group-item-action">
<i class="bi bi-journal-text"></i> Blog
</a>
<a href="/admin/portfolio/categories" class="list-group-item list-group-item-action">
<i class="bi bi-images"></i> Portfolio
</a>
<a href="/admin/settings" class="list-group-item list-group-item-action">
<i class="bi bi-gear"></i> Settings
</a>
<a href="/admin/users" class="list-group-item list-group-item-action">
<i class="bi bi-people"></i> Users
</a>
</div>
<div class="mt-4">
<h5>JavaScript Test</h5>
<button class="btn btn-primary" onclick="alert('JavaScript is working!'); return false;">
Test JavaScript
</button>
<button class="btn btn-success" onclick="window.location.href='/admin/pages'">
Navigate to Pages (JS)
</button>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header bg-info text-white">
<h4>Browser Information</h4>
</div>
<div class="card-body">
<p id="userAgent"></p>
<p id="cookiesEnabled"></p>
<p id="javaScriptEnabled">JavaScript: Enabled</p>
</div>
</div>
</div>
</div>
</div>
<script>
document.getElementById('userAgent').textContent = 'User Agent: ' + navigator.userAgent;
document.getElementById('cookiesEnabled').textContent = 'Cookies: ' + (navigator.cookieEnabled ? 'Enabled' : 'Disabled');
// Test click events
document.querySelectorAll('.list-group-item').forEach(function(item) {
item.addEventListener('click', function(e) {
console.log('Link clicked:', this.href);
});
});
</script>

153
Views/AdminBlog/Create.cshtml Executable file
View File

@@ -0,0 +1,153 @@
@model SkyArtShop.Models.BlogPost
@{
Layout = "~/Views/Shared/_AdminLayout.cshtml";
ViewData["Title"] = "Create Blog Post";
}
<style>
.form-check-input[type="checkbox"] {
width: 22px;
height: 22px;
cursor: pointer;
border: 2px solid #dee2e6;
border-radius: 4px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-color: white;
transition: all 0.2s ease;
position: relative;
}
.form-check-input[type="checkbox"]:checked {
background-color: #28a745;
border-color: #28a745;
}
.form-check-input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 16px;
font-weight: bold;
line-height: 1;
}
.form-check-input[type="checkbox"]:hover {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
</style>
<div class="card">
<div class="card-body">
<form method="post">
<div asp-validation-summary="All" class="text-danger mb-3"></div>
<div class="mb-3">
<label class="form-label">Title</label>
<input class="form-control" name="Title" value="@Model.Title" required />
</div>
<div class="mb-3">
<label class="form-label">Content</label>
<textarea class="form-control" name="Content" id="blogContent" rows="15">@Model.Content</textarea>
</div>
<div class="mb-3">
<label class="form-label">Excerpt</label>
<textarea class="form-control" name="Excerpt" rows="3">@Model.Excerpt</textarea>
</div>
<div class="mb-3">
<label class="form-label">Featured Image URL</label>
<div class="input-group">
<input class="form-control" name="FeaturedImage" id="featuredImageUrl" value="@Model.FeaturedImage" />
<button type="button" class="btn btn-secondary" onclick="uploadFeaturedImage()">Upload</button>
</div>
<div id="imagePreview" class="mt-2" style="@(string.IsNullOrEmpty(Model.FeaturedImage) ? "display:none;" : "")">
<img src="@Model.FeaturedImage" style="max-width: 200px; max-height: 200px;" />
</div>
</div>
<div class="mb-3">
<label class="form-label">Tags (comma separated)</label>
<input class="form-control" name="Tags" value="@(Model.Tags != null ? string.Join(", ", Model.Tags) : "")" />
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="IsPublished" @(Model.IsPublished ? "checked" : "") />
<label class="form-check-label">Published</label>
</div>
<button class="btn btn-primary" type="submit">Save Post</button>
<a class="btn btn-secondary" href="/admin/blog">Cancel</a>
</form>
</div>
</div>
@section Scripts {
<script src="https://cdn.ckeditor.com/ckeditor5/40.1.0/classic/ckeditor.js"></script>
<script>
ClassicEditor
.create(document.querySelector('#blogContent'), {
toolbar: {
items: [
'heading', '|',
'bold', 'italic', 'underline', 'strikethrough', '|',
'link', 'blockQuote', '|',
'bulletedList', 'numberedList', '|',
'outdent', 'indent', '|',
'alignment', '|',
'insertTable', '|',
'fontSize', 'fontColor', 'fontBackgroundColor', '|',
'removeFormat', '|',
'undo', 'redo', '|',
'sourceEditing'
],
shouldNotGroupWhenFull: true
},
heading: {
options: [
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
{ model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' },
{ model: 'heading4', view: 'h4', title: 'Heading 4', class: 'ck-heading_heading4' }
]
},
fontSize: {
options: ['small', 'default', 'big']
},
table: {
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells']
},
htmlSupport: {
allow: [{ name: /.*/, attributes: true, classes: true, styles: true }]
}
})
.catch(error => { console.error(error); });
function uploadFeaturedImage() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = function(e) {
const file = e.target.files[0];
const formData = new FormData();
formData.append('file', file);
fetch('/admin/upload/image', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(result => {
if (result.success) {
document.getElementById('featuredImageUrl').value = result.url;
document.getElementById('imagePreview').style.display = 'block';
document.getElementById('imagePreview').innerHTML = '<img src="' + result.url + '" style="max-width: 200px; max-height: 200px;" />';
} else {
alert('Upload failed: ' + result.message);
}
});
};
input.click();
}
</script>
}

153
Views/AdminBlog/Edit.cshtml Executable file
View File

@@ -0,0 +1,153 @@
@model SkyArtShop.Models.BlogPost
@{
Layout = "~/Views/Shared/_AdminLayout.cshtml";
ViewData["Title"] = "Edit Blog Post";
}
<style>
.form-check-input[type="checkbox"] {
width: 22px;
height: 22px;
cursor: pointer;
border: 2px solid #dee2e6;
border-radius: 4px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-color: white;
transition: all 0.2s ease;
position: relative;
}
.form-check-input[type="checkbox"]:checked {
background-color: #28a745;
border-color: #28a745;
}
.form-check-input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 16px;
font-weight: bold;
line-height: 1;
}
.form-check-input[type="checkbox"]:hover {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
</style>
<div class="card">
<div class="card-body">
<form method="post">
<div asp-validation-summary="All" class="text-danger mb-3"></div>
<div class="mb-3">
<label class="form-label">Title</label>
<input class="form-control" name="Title" value="@Model.Title" required />
</div>
<div class="mb-3">
<label class="form-label">Content</label>
<textarea class="form-control" name="Content" id="blogContent" rows="15">@Model.Content</textarea>
</div>
<div class="mb-3">
<label class="form-label">Excerpt</label>
<textarea class="form-control" name="Excerpt" rows="3">@Model.Excerpt</textarea>
</div>
<div class="mb-3">
<label class="form-label">Featured Image URL</label>
<div class="input-group">
<input class="form-control" name="FeaturedImage" id="featuredImageUrl" value="@Model.FeaturedImage" />
<button type="button" class="btn btn-secondary" onclick="uploadFeaturedImage()">Upload</button>
</div>
<div id="imagePreview" class="mt-2" style="@(string.IsNullOrEmpty(Model.FeaturedImage) ? "display:none;" : "")">
<img src="@Model.FeaturedImage" style="max-width: 200px; max-height: 200px;" />
</div>
</div>
<div class="mb-3">
<label class="form-label">Tags (comma separated)</label>
<input class="form-control" name="Tags" value="@(Model.Tags != null ? string.Join(", ", Model.Tags) : "")" />
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="IsPublished" @(Model.IsPublished ? "checked" : "") />
<label class="form-check-label">Published</label>
</div>
<button class="btn btn-primary" type="submit">Save Changes</button>
<a class="btn btn-secondary" href="/admin/blog">Cancel</a>
</form>
</div>
</div>
@section Scripts {
<script src="https://cdn.ckeditor.com/ckeditor5/40.1.0/classic/ckeditor.js"></script>
<script>
ClassicEditor
.create(document.querySelector('#blogContent'), {
toolbar: {
items: [
'heading', '|',
'bold', 'italic', 'underline', 'strikethrough', '|',
'link', 'blockQuote', '|',
'bulletedList', 'numberedList', '|',
'outdent', 'indent', '|',
'alignment', '|',
'insertTable', '|',
'fontSize', 'fontColor', 'fontBackgroundColor', '|',
'removeFormat', '|',
'undo', 'redo', '|',
'sourceEditing'
],
shouldNotGroupWhenFull: true
},
heading: {
options: [
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
{ model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' },
{ model: 'heading4', view: 'h4', title: 'Heading 4', class: 'ck-heading_heading4' }
]
},
fontSize: {
options: ['small', 'default', 'big']
},
table: {
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells']
},
htmlSupport: {
allow: [{ name: /.*/, attributes: true, classes: true, styles: true }]
}
})
.catch(error => { console.error(error); });
function uploadFeaturedImage() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = function(e) {
const file = e.target.files[0];
const formData = new FormData();
formData.append('file', file);
fetch('/admin/upload/image', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(result => {
if (result.success) {
document.getElementById('featuredImageUrl').value = result.url;
document.getElementById('imagePreview').style.display = 'block';
document.getElementById('imagePreview').innerHTML = '<img src="' + result.url + '" style="max-width: 200px; max-height: 200px;" />';
} else {
alert('Upload failed: ' + result.message);
}
});
};
input.click();
}
</script>
}

172
Views/AdminHomepage/Create.cshtml Executable file
View File

@@ -0,0 +1,172 @@
@model SkyArtShop.Models.HomepageSection
@{
ViewData["Title"] = "Create Homepage Section";
Layout = "_AdminLayout";
}
<style>
.form-check-input[type="checkbox"] {
width: 22px;
height: 22px;
cursor: pointer;
border: 2px solid #dee2e6;
border-radius: 4px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-color: white;
transition: all 0.2s ease;
position: relative;
}
.form-check-input[type="checkbox"]:checked {
background-color: #28a745;
border-color: #28a745;
}
.form-check-input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 16px;
font-weight: bold;
line-height: 1;
}
.form-check-input[type="checkbox"]:hover {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
</style>
<div class="mb-4">
<a href="/admin/homepage" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Homepage Editor
</a>
</div>
<div class="card">
<div class="card-header bg-success text-white">
<h4 class="mb-0"><i class="bi bi-plus-circle"></i> Create New Homepage Section</h4>
</div>
<div class="card-body">
<form method="post" action="/admin/homepage/section/create" enctype="multipart/form-data">
@Html.AntiForgeryToken()
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="SectionType" class="form-label">Section Type <span class="text-danger">*</span></label>
<select id="SectionType" name="SectionType" class="form-select" required>
<option value="">-- Select Section Type --</option>
<option value="hero">Hero Section</option>
<option value="inspiration">Inspiration Section</option>
<option value="collection">Collection Section</option>
<option value="promotion">Promotion Section</option>
<option value="custom">Custom Section</option>
</select>
<small class="text-muted">Choose the type of content section you want to add</small>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Status</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="IsActive" name="IsActive" value="true" checked>
<label class="form-check-label" for="IsActive">Active (visible on homepage)</label>
</div>
</div>
</div>
</div>
<div class="mb-3">
<label for="Title" class="form-label">Section Title <span class="text-danger">*</span></label>
<input type="text" id="Title" name="Title" class="form-control" placeholder="Enter section title" required />
</div>
<div class="mb-3">
<label for="Subtitle" class="form-label">Subtitle</label>
<input type="text" id="Subtitle" name="Subtitle" class="form-control" placeholder="Enter subtitle (optional)" />
</div>
<div class="mb-3">
<label for="Content" class="form-label">Content</label>
<textarea id="Content" name="Content" class="form-control" rows="6" placeholder="Enter your content here..."></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="ButtonText" class="form-label">Button Text</label>
<input type="text" id="ButtonText" name="ButtonText" class="form-control" placeholder="e.g., Shop Now, Learn More" />
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="ButtonUrl" class="form-label">Button URL</label>
<input type="text" id="ButtonUrl" name="ButtonUrl" class="form-control" placeholder="e.g., /Shop, /Contact" />
</div>
</div>
</div>
<div class="mb-3">
<label for="imageFile" class="form-label">Section Image</label>
<input type="file" id="imageFile" name="imageFile" class="form-control" accept="image/*" />
<small class="text-muted">Supported formats: JPG, PNG, GIF (max 5MB)</small>
</div>
<hr class="my-4" />
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> <strong>Note:</strong> This section will be added to the end of your homepage. You can reorder it by dragging on the main editor page.
</div>
<div class="d-flex justify-content-between">
<a href="/admin/homepage" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-success btn-lg">
<i class="bi bi-plus-circle"></i> Create Section
</button>
</div>
</form>
</div>
</div>
@section Scripts
{
<script>
let contentEditor;
ClassicEditor
.create(document.querySelector('#Content'), {
toolbar: [
'heading', '|',
'bold', 'italic', '|',
'link', 'bulletedList', 'numberedList', '|',
'indent', 'outdent', '|',
'blockQuote', 'insertTable', '|',
'undo', 'redo'
],
heading: {
options: [
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
{ model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' }
]
}
})
.then(editor => {
contentEditor = editor;
document.querySelector('form').addEventListener('submit', function(e) {
document.querySelector('#Content').value = contentEditor.getData();
});
})
.catch(error => {
console.error('CKEditor initialization error:', error);
});
</script>
}

233
Views/AdminHomepage/Edit.cshtml Executable file
View File

@@ -0,0 +1,233 @@
@model SkyArtShop.Models.HomepageSection
@{
ViewData["Title"] = "Edit Homepage Section";
Layout = "_AdminLayout";
}
<style>
.form-check-input[type="checkbox"] {
width: 22px !important;
height: 22px !important;
cursor: pointer !important;
border: 2px solid #dee2e6 !important;
border-radius: 4px !important;
appearance: none !important;
-webkit-appearance: none !important;
-moz-appearance: none !important;
background-color: white !important;
background-image: none !important;
transition: all 0.2s ease !important;
position: relative !important;
}
.form-check-input[type="checkbox"]:checked {
background-color: #28a745 !important;
border-color: #28a745;
}
.form-check-input[type="checkbox"]:checked::after {
content: '✓';
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
color: white !important;
font-size: 16px !important;
font-weight: bold !important;
line-height: 1 !important;
display: block !important;
}
.form-check-input[type="checkbox"]:hover {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25) !important;
}
</style>
<div class="mb-4">
<a href="/admin/homepage" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Homepage Editor
</a>
</div>
<div class="card">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">Edit Section: @Model.Title</h4>
</div>
<div class="card-body">
<form method="post" action="/admin/homepage/section/update" enctype="multipart/form-data">
@Html.AntiForgeryToken()
<input type="hidden" name="Id" value="@Model.Id" />
<input type="hidden" name="DisplayOrder" value="@Model.DisplayOrder" />
<input type="hidden" name="CreatedAt" value="@Model.CreatedAt" />
<input type="hidden" id="ImageUrl" name="ImageUrl" value="@Model.ImageUrl" />
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="SectionType" class="form-label">Section Type <span class="text-danger">*</span></label>
<select id="SectionType" name="SectionType" class="form-select" required>
<option value="hero" selected="@(Model.SectionType == "hero")">Hero Section</option>
<option value="inspiration" selected="@(Model.SectionType == "inspiration")">Inspiration Section</option>
<option value="collection" selected="@(Model.SectionType == "collection")">Collection Section</option>
<option value="promotion" selected="@(Model.SectionType == "promotion")">Promotion Section</option>
<option value="custom" selected="@(Model.SectionType == "custom")">Custom Section</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Status</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="IsActive" name="IsActive" value="true" checked="@Model.IsActive">
<label class="form-check-label" for="IsActive">Active (visible on homepage)</label>
</div>
</div>
</div>
</div>
<div class="mb-3">
<label for="Title" class="form-label">Section Title <span class="text-danger">*</span></label>
<input type="text" id="Title" name="Title" class="form-control" value="@Model.Title" required />
</div>
<div class="mb-3">
<label for="Subtitle" class="form-label">Subtitle</label>
<input type="text" id="Subtitle" name="Subtitle" class="form-control" value="@Model.Subtitle" />
</div>
<div class="mb-3">
<label for="Content" class="form-label">Content</label>
<textarea id="Content" name="Content" class="form-control" rows="6">@Model.Content</textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="ButtonText" class="form-label">Button Text</label>
<input type="text" id="ButtonText" name="ButtonText" class="form-control" value="@Model.ButtonText" placeholder="e.g., Shop Now, Learn More" />
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="ButtonUrl" class="form-label">Button URL</label>
<input type="text" id="ButtonUrl" name="ButtonUrl" class="form-control" value="@Model.ButtonUrl" placeholder="e.g., /Shop, /Contact" />
</div>
</div>
</div>
<div class="mb-3">
<label for="imageFile" class="form-label">Section Image</label>
<div class="mb-2" id="currentImagePreview" style="@(string.IsNullOrEmpty(Model.ImageUrl) ? "display: none;" : "")">
<img src="@(Model.ImageUrl ?? "")" alt="Current image" style="max-width: 300px; max-height: 200px; border: 1px solid #ddd; border-radius: 4px !important;" id="sectionImagePreview" />
<p class="text-muted small mt-1">Current image</p>
</div>
<input type="hidden" id="SelectedImageUrl" name="SelectedImageUrl" value="" />
<button type="button" class="btn btn-primary" onclick="openImagePicker(handleSectionImageSelection, 'single')">
<i class="bi bi-images"></i> Select/Upload Image
</button>
<small class="text-muted d-block mt-1">Select from library or upload new image</small>
</div>
<hr class="my-4" />
<div class="d-flex justify-content-between">
<a href="/admin/homepage" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-check-circle"></i> Save Changes
</button>
</div>
</form>
</div>
</div>
@section Scripts
{
<script>
// Handle Section Image Selection
function handleSectionImageSelection(selectedUrls) {
if (!selectedUrls || selectedUrls.length === 0) {
alert('No images selected. Please try again.');
return;
}
try {
const imageUrl = selectedUrls[0];
const selectedInput = document.getElementById('SelectedImageUrl');
const previewImg = document.getElementById('sectionImagePreview');
const previewDiv = document.getElementById('currentImagePreview');
if (!selectedInput || !previewImg || !previewDiv) {
console.error('Required DOM elements not found');
return;
}
selectedInput.value = imageUrl;
previewImg.src = imageUrl;
previewDiv.style.display = 'block';
} catch (error) {
console.error('Error in handleSectionImageSelection:', error);
alert('Error setting image: ' + error.message);
}
}
// Ensure checkbox value is properly set before form submission
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
const isActiveCheckbox = document.getElementById('IsActive');
if (form && isActiveCheckbox) {
form.addEventListener('submit', function(e) {
// Set the hidden input value based on checkbox state
if (isActiveCheckbox.checked) {
isActiveCheckbox.value = 'true';
} else {
// Add a hidden input to ensure false is submitted
const hiddenFalse = document.createElement('input');
hiddenFalse.type = 'hidden';
hiddenFalse.name = 'IsActive';
hiddenFalse.value = 'false';
form.appendChild(hiddenFalse);
}
});
}
});
let contentEditor;
ClassicEditor
.create(document.querySelector('#Content'), {
toolbar: [
'heading', '|',
'bold', 'italic', '|',
'link', 'bulletedList', 'numberedList', '|',
'indent', 'outdent', '|',
'blockQuote', 'insertTable', '|',
'undo', 'redo'
],
heading: {
options: [
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
{ model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' }
]
}
})
.then(editor => {
contentEditor = editor;
document.querySelector('form').addEventListener('submit', function(e) {
// Sync CKEditor content to textarea before submit
const contentTextarea = document.querySelector('#Content');
if (contentTextarea && contentEditor) {
contentTextarea.value = contentEditor.getData();
console.log('Form submitted with content:', contentTextarea.value.substring(0, 100));
}
});
})
.catch(error => {
console.error('CKEditor initialization error:', error);
});
</script>
}

252
Views/AdminHomepage/Index.cshtml Executable file
View File

@@ -0,0 +1,252 @@
@model List<SkyArtShop.Models.HomepageSection>
@{
ViewData["Title"] = "Homepage Editor";
Layout = "_AdminLayout";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Homepage Editor</h2>
<a href="/admin/homepage/section/create" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add New Section
</a>
</div>
@if (TempData["SuccessMessage"] != null)
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
@TempData["SuccessMessage"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
<!-- Footer Editor -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-footer"></i> Footer Text</h5>
</div>
<div class="card-body">
<form method="post" action="/admin/homepage/footer/update">
@Html.AntiForgeryToken()
<div class="mb-3">
<textarea id="footerText" name="footerText" class="form-control" rows="3">@ViewBag.Settings.FooterText</textarea>
</div>
<button type="submit" class="btn btn-success">
<i class="bi bi-check-circle"></i> Save Footer
</button>
</form>
</div>
</div>
<!-- Homepage Sections -->
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-layout-text-window-reverse"></i> Homepage Sections</h5>
<small>Drag and drop to reorder sections</small>
</div>
<div class="card-body">
@if (Model != null && Model.Any())
{
<div id="sortable-sections" class="list-group">
@foreach (var sect in Model)
{
<div class="list-group-item section-item" data-id="@sect.Id">
<div class="row align-items-center">
<div class="col-md-1 text-center drag-handle" style="cursor: grab;">
<i class="bi bi-grip-vertical" style="font-size: 1.5rem; color: #6c757d;"></i>
</div>
<div class="col-md-2">
<span class="badge bg-secondary">@sect.SectionType</span>
@if (!sect.IsActive)
{
<span class="badge bg-warning ms-1">Inactive</span>
}
</div>
<div class="col-md-4">
<strong>@sect.Title</strong>
@if (!string.IsNullOrEmpty(sect.Subtitle))
{
<br /><small class="text-muted">@sect.Subtitle</small>
}
</div>
<div class="col-md-2 text-center">
<small class="text-muted">Order: @sect.DisplayOrder</small>
</div>
<div class="col-md-3 text-end">
<div class="d-flex gap-2 justify-content-end">
<a href="/admin/homepage/section/@sect.Id" class="btn btn-sm btn-outline-primary" title="Edit Section">
<i class="bi bi-pencil"></i> Edit
</a>
<form method="post" action="/admin/homepage/section/toggle/@sect.Id" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-outline-@(sect.IsActive ? "warning" : "success")" title="@(sect.IsActive ? "Deactivate" : "Activate")">
<i class="bi bi-@(sect.IsActive ? "eye-slash" : "eye")"></i>
</button>
</form>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteSection('@sect.Id')" title="Delete Section">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</div>
}
</div>
}
else
{
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> No sections found. Click "Add New Section" to create your first homepage section.
</div>
}
</div>
</div>
<!-- Preview Button -->
<div class="mt-4">
<a href="/" target="_blank" class="btn btn-secondary btn-lg">
<i class="bi bi-eye"></i> Preview Homepage
</a>
</div>
@section Scripts
{
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize CKEditor for Footer (if it exists)
const footerTextarea = document.querySelector('#footerText');
if (footerTextarea && typeof ClassicEditor !== 'undefined') {
let footerEditor;
ClassicEditor
.create(footerTextarea, {
toolbar: ['bold', 'italic', 'link']
})
.then(editor => {
footerEditor = editor;
const footerForm = footerTextarea.closest('form');
if (footerForm) {
footerForm.addEventListener('submit', function(e) {
footerTextarea.value = footerEditor.getData();
});
}
})
.catch(error => {
console.error('CKEditor initialization error:', error);
});
}
// Initialize Sortable for drag & drop
const sortableList = document.getElementById('sortable-sections');
if (sortableList) {
const sortable = Sortable.create(sortableList, {
animation: 200,
ghostClass: 'sortable-ghost',
dragClass: 'sortable-drag',
handle: '.drag-handle',
draggable: '.section-item',
onStart: function(evt) {
evt.item.style.cursor = 'grabbing';
},
onEnd: function (evt) {
evt.item.style.cursor = '';
const sectionIds = Array.from(sortableList.children).map(item => item.getAttribute('data-id'));
fetch('/admin/homepage/section/reorder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
},
body: JSON.stringify(sectionIds)
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update display order numbers
sortableList.querySelectorAll('.section-item').forEach((item, index) => {
item.querySelector('.col-md-2.text-center small').textContent = 'Order: ' + index;
});
}
})
.catch(error => {
console.error('Error updating section order:', error);
});
}
});
} else {
console.log('sortable-sections element not found');
}
});
function deleteSection(id) {
if (confirm('Are you sure you want to delete this section?')) {
const form = document.createElement('form');
form.method = 'POST';
form.action = '/admin/homepage/section/delete/' + id;
const token = document.querySelector('input[name="__RequestVerificationToken"]').cloneNode();
form.appendChild(token);
document.body.appendChild(form);
form.submit();
}
}
</script>
<style>
.section-item {
transition: all 0.3s ease;
margin-bottom: 12px;
border-left: 4px solid #6c757d;
background: white;
padding: 15px;
border-radius: 6px;
}
.section-item:hover {
background-color: #f8f9fa;
border-left-color: #0d6efd;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.drag-handle {
transition: all 0.2s ease;
cursor: grab;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.drag-handle:hover {
transform: scale(1.1);
color: #0d6efd !important;
cursor: grab;
}
.drag-handle:active {
cursor: grabbing !important;
}
#sortable-sections {
list-style: none;
padding: 0;
}
.sortable-ghost {
opacity: 0.5;
background: #e3f2fd !important;
border: 2px dashed #0d6efd !important;
}
.sortable-drag {
opacity: 0.8;
cursor: grabbing !important;
transform: rotate(2deg);
box-shadow: 0 5px 15px rgba(0,0,0,0.3) !important;
}
.sortable-fallback {
opacity: 0.8;
background: white !important;
box-shadow: 0 5px 20px rgba(0,0,0,0.3) !important;
}
.btn-group .btn, .d-flex .btn {
min-width: 75px;
}
.list-group-item {
border: 1px solid #dee2e6;
}
</style>
}

81
Views/AdminMenu/Create.cshtml Executable file
View File

@@ -0,0 +1,81 @@
@model SkyArtShop.Models.MenuItem
@{
Layout = "~/Views/Shared/_AdminLayout.cshtml";
ViewData["Title"] = "Create Menu Item";
}
<div class="mb-4">
<a href="/admin/menu" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Back to Menu
</a>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">Create Menu Item</h5>
</div>
<div class="card-body">
<form method="post" action="/admin/menu/create">
<div asp-validation-summary="All" class="text-danger mb-3"></div>
<div class="mb-3">
<label for="Label" class="form-label">Label *</label>
<input type="text" class="form-control" id="Label" name="Label" value="@Model.Label" required>
<small class="form-text text-muted">The text that will appear in the navigation menu</small>
</div>
<div class="mb-3">
<label for="Url" class="form-label">URL *</label>
<input type="text" class="form-control" id="Url" name="Url" value="@Model.Url" required>
<small class="form-text text-muted">Examples: /, /Shop, /About, /#promotion, #instagram</small>
</div>
<div class="mb-3">
<label for="DisplayOrder" class="form-label">Display Order</label>
<input type="number" class="form-control" id="DisplayOrder" name="DisplayOrder" value="@Model.DisplayOrder" min="0">
<small class="form-text text-muted">Lower numbers appear first</small>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="IsActive" name="IsActive" value="true" checked>
<input type="hidden" name="IsActive" value="false">
<label class="form-check-label" for="IsActive">
Active (Globally enable this menu item)
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="ShowInNavbar" name="ShowInNavbar" value="true" checked>
<input type="hidden" name="ShowInNavbar" value="false">
<label class="form-check-label" for="ShowInNavbar">
Show in Desktop Navbar
</label>
<small class="form-text text-muted d-block">Display in the horizontal navigation bar at the top</small>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="ShowInDropdown" name="ShowInDropdown" value="true" checked>
<input type="hidden" name="ShowInDropdown" value="false">
<label class="form-check-label" for="ShowInDropdown">
Show in Hamburger Dropdown
</label>
<small class="form-text text-muted d-block">Display in the mobile menu and desktop hamburger dropdown</small>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="OpenInNewTab" name="OpenInNewTab" value="true">
<input type="hidden" name="OpenInNewTab" value="false">
<label class="form-check-label" for="OpenInNewTab">
Open in new tab
</label>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Create Menu Item
</button>
<a href="/admin/menu" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>

136
Views/AdminMenu/Edit.cshtml Executable file
View File

@@ -0,0 +1,136 @@
@model SkyArtShop.Models.MenuItem
@{
Layout = "~/Views/Shared/_AdminLayout.cshtml";
ViewData["Title"] = "Edit Menu Item";
}
<style>
/* Custom checkbox styling with green fill and white checkmark */
.menu-checkbox {
width: 24px;
height: 24px;
cursor: pointer;
border: 2px solid #6c757d;
border-radius: 4px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-color: white;
transition: all 0.2s ease;
}
.menu-checkbox:checked {
background-color: #28a745;
border-color: #28a745;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
background-size: 16px;
}
.menu-checkbox:hover {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
.menu-checkbox:focus {
outline: none;
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
</style>
<div class="mb-4">
<a href="/admin/menu" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Back to Menu
</a>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">Edit Menu Item</h5>
</div>
<div class="card-body">
<form method="post" action="/admin/menu/edit/@Model.Id">
<div asp-validation-summary="All" class="text-danger mb-3"></div>
<div class="mb-3">
<label for="Label" class="form-label">Label *</label>
<input type="text" class="form-control" id="Label" name="Label" value="@Model.Label" required>
<small class="form-text text-muted">The text that will appear in the navigation menu</small>
</div>
<div class="mb-3">
<label for="Url" class="form-label">URL *</label>
<input type="text" class="form-control" id="Url" name="Url" value="@Model.Url" required>
<small class="form-text text-muted">Examples: /, /Shop, /About, /#promotion, #instagram</small>
</div>
<div class="mb-3">
<label for="DisplayOrder" class="form-label">Display Order</label>
<input type="number" class="form-control" id="DisplayOrder" name="DisplayOrder" value="@Model.DisplayOrder" min="0">
<small class="form-text text-muted">Lower numbers appear first</small>
</div>
<div class="card mb-3">
<div class="card-header bg-light">
<strong>Menu Item Settings</strong>
</div>
<div class="card-body">
<div class="form-check mb-3 p-3 border rounded" style="background-color: #f8f9fa;">
<input class="menu-checkbox" type="checkbox" id="IsActive" name="IsActive" value="true" @(Model.IsActive ? "checked" : "")>
<input type="hidden" name="IsActive" value="false">
<label class="form-check-label ms-2" for="IsActive" style="cursor: pointer; font-weight: 500;">
<i class="bi bi-power text-success"></i> Active (Globally enable this menu item)
</label>
<div class="ms-4 mt-1">
<small class="text-muted">Current status: <strong class="@(Model.IsActive ? "text-success" : "text-danger")">@(Model.IsActive ? "✓ Enabled" : "✗ Disabled")</strong></small>
</div>
</div>
<div class="form-check mb-3 p-3 border rounded" style="background-color: #f8f9fa;">
<input class="menu-checkbox" type="checkbox" id="ShowInNavbar" name="ShowInNavbar" value="true" @(Model.ShowInNavbar ? "checked" : "")>
<input type="hidden" name="ShowInNavbar" value="false">
<label class="form-check-label ms-2" for="ShowInNavbar" style="cursor: pointer; font-weight: 500;">
<i class="bi bi-window-desktop text-primary"></i> Show in Desktop Navbar
</label>
<div class="ms-4 mt-1">
<small class="text-muted">Display in the horizontal navigation bar at the top</small><br>
<small class="text-muted">Current status: <strong class="@(Model.ShowInNavbar ? "text-success" : "text-danger")">@(Model.ShowInNavbar ? "✓ Visible" : "✗ Hidden")</strong></small>
</div>
</div>
<div class="form-check mb-3 p-3 border rounded" style="background-color: #f8f9fa;">
<input class="menu-checkbox" type="checkbox" id="ShowInDropdown" name="ShowInDropdown" value="true" @(Model.ShowInDropdown ? "checked" : "")>
<input type="hidden" name="ShowInDropdown" value="false">
<label class="form-check-label ms-2" for="ShowInDropdown" style="cursor: pointer; font-weight: 500;">
<i class="bi bi-list text-info"></i> Show in Hamburger Dropdown
</label>
<div class="ms-4 mt-1">
<small class="text-muted">Display in the mobile menu and desktop hamburger dropdown</small><br>
<small class="text-muted">Current status: <strong class="@(Model.ShowInDropdown ? "text-success" : "text-danger")">@(Model.ShowInDropdown ? "✓ Visible" : "✗ Hidden")</strong></small>
</div>
</div>
<div class="form-check mb-0 p-3 border rounded" style="background-color: #f8f9fa;">
<input class="menu-checkbox" type="checkbox" id="OpenInNewTab" name="OpenInNewTab" value="true" @(Model.OpenInNewTab ? "checked" : "")>
<input type="hidden" name="OpenInNewTab" value="false">
<label class="form-check-label ms-2" for="OpenInNewTab" style="cursor: pointer; font-weight: 500;">
<i class="bi bi-box-arrow-up-right text-warning"></i> Open in new tab
</label>
<div class="ms-4 mt-1">
<small class="text-muted">Current status: <strong class="@(Model.OpenInNewTab ? "text-success" : "text-danger")">@(Model.OpenInNewTab ? "✓ New Tab" : "✗ Same Tab")</strong></small>
</div>
</div>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Update Menu Item
</button>
<a href="/admin/menu" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>

86
Views/AdminMenu/Index.cshtml Executable file
View File

@@ -0,0 +1,86 @@
@model List<SkyArtShop.Models.MenuItem>
@{
Layout = "~/Views/Shared/_AdminLayout.cshtml";
ViewData["Title"] = "Manage Menu";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Menu Items</h2>
<div>
<form method="post" action="/admin/menu/reseed" style="display:inline;" onsubmit="return confirm('This will delete all existing menu items and create new ones. Continue?')">
<button type="submit" class="btn btn-warning">Reseed Menu</button>
</form>
<a href="/admin/menu/create" class="btn btn-primary">Add Menu Item</a>
</div>
</div>
@if (TempData["SuccessMessage"] != null)
{
<div class="alert alert-success">@TempData["SuccessMessage"]</div>
}
<div class="card">
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>Order</th>
<th>Label</th>
<th>URL</th>
<th>Status</th>
<th>Navbar</th>
<th>Dropdown</th>
<th>New Tab</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>@item.DisplayOrder</td>
<td>@item.Label</td>
<td>@item.Url</td>
<td>
@if (item.IsActive)
{
<span class="badge bg-success"><i class="bi bi-check-circle-fill"></i> Active</span>
}
else
{
<span class="badge bg-danger"><i class="bi bi-x-circle-fill"></i> Inactive</span>
}
</td>
<td>
@if (item.ShowInNavbar)
{
<span class="badge bg-primary">Yes</span>
}
else
{
<span class="badge bg-light text-dark">No</span>
}
</td>
<td>
@if (item.ShowInDropdown)
{
<span class="badge bg-info">Yes</span>
}
else
{
<span class="badge bg-light text-dark">No</span>
}
</td>
<td>@(item.OpenInNewTab ? "Yes" : "No")</td>
<td>
<a href="/admin/menu/edit/@item.Id" class="btn btn-sm btn-warning">Edit</a>
<form method="post" action="/admin/menu/delete/@item.Id" style="display:inline;" onsubmit="return confirm('Delete this menu item?')">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>

133
Views/AdminPages/Create.cshtml Executable file
View File

@@ -0,0 +1,133 @@
@model SkyArtShop.Models.Page
@{
Layout = "~/Views/Shared/_AdminLayout.cshtml";
ViewData["Title"] = "Create Page";
}
<style>
.form-check-input[type="checkbox"] {
width: 22px !important;
height: 22px !important;
cursor: pointer !important;
border: 2px solid #dee2e6 !important;
border-radius: 4px !important;
appearance: none !important;
-webkit-appearance: none !important;
-moz-appearance: none !important;
background-color: white !important;
background-image: none !important;
transition: all 0.2s ease !important;
position: relative !important;
}
.form-check-input[type="checkbox"]:checked {
background-color: #28a745 !important;
border-color: #28a745 !important;
background-image: none !important;
}
.form-check-input[type="checkbox"]:checked::after {
content: '✓' !important;
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
color: white !important;
font-size: 16px !important;
font-weight: bold !important;
line-height: 1 !important;
display: block !important;
}
.form-check-input[type="checkbox"]:hover {
border-color: #28a745 !important;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25) !important;
}
</style>
<div class="card">
<div class="card-body">
<form method="post">
<div asp-validation-summary="All" class="text-danger mb-3"></div>
<div class="mb-3">
<label class="form-label">Page Name</label>
<input class="form-control" asp-for="PageName" required />
</div>
<div class="mb-3">
<label class="form-label">Title</label>
<input class="form-control" asp-for="Title" />
</div>
<div class="mb-3">
<label class="form-label">Subtitle</label>
<input class="form-control" asp-for="Subtitle" />
</div>
<div class="mb-3">
<label class="form-label">Content</label>
<textarea class="form-control" asp-for="Content" id="pageContent" rows="15"></textarea>
</div>
<div class="form-check mb-3">
<input asp-for="IsActive" class="form-check-input" type="checkbox" />
<label class="form-check-label">Active</label>
</div>
<button class="btn btn-primary" type="submit">Save Page</button>
<a class="btn btn-secondary" href="/admin/pages">Cancel</a>
</form>
</div>
</div>
@section Scripts {
<script src="https://cdn.ckeditor.com/ckeditor5/40.1.0/classic/ckeditor.js"></script>
<script>
ClassicEditor
.create(document.querySelector('#pageContent'), {
toolbar: {
items: [
'heading', '|',
'bold', 'italic', 'underline', 'strikethrough', '|',
'link', 'blockQuote', '|',
'bulletedList', 'numberedList', '|',
'outdent', 'indent', '|',
'alignment', '|',
'insertTable', '|',
'fontSize', 'fontColor', 'fontBackgroundColor', '|',
'removeFormat', '|',
'undo', 'redo', '|',
'sourceEditing'
],
shouldNotGroupWhenFull: true
},
heading: {
options: [
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
{ model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' },
{ model: 'heading4', view: 'h4', title: 'Heading 4', class: 'ck-heading_heading4' }
]
},
fontSize: {
options: [
'small',
'default',
'big'
]
},
table: {
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells']
},
htmlSupport: {
allow: [
{
name: /.*/,
attributes: true,
classes: true,
styles: true
}
]
}
})
.catch(error => {
console.error(error);
});
</script>
}

489
Views/AdminPages/Edit.cshtml Executable file
View File

@@ -0,0 +1,489 @@
@model SkyArtShop.Models.Page
@{
Layout = "~/Views/Shared/_AdminLayout.cshtml";
ViewData["Title"] = "Edit Page";
}
<style>
.form-check-input[type="checkbox"] {
width: 22px !important;
height: 22px !important;
cursor: pointer !important;
border: 2px solid #dee2e6 !important;
border-radius: 4px !important;
appearance: none !important;
-webkit-appearance: none !important;
-moz-appearance: none !important;
background-color: white !important;
background-image: none !important;
transition: all 0.2s ease !important;
position: relative !important;
}
.form-check-input[type="checkbox"]:checked {
background-color: #28a745 !important;
border-color: #28a745 !important;
background-image: none !important;
}
.form-check-input[type="checkbox"]:checked::after {
content: '✓' !important;
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
color: white !important;
font-size: 16px !important;
font-weight: bold !important;
line-height: 1 !important;
display: block !important;
}
.form-check-input[type="checkbox"]:hover {
border-color: #28a745 !important;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25) !important;
}
</style>
<div class="card">
<div class="card-body">
<form method="post" enctype="multipart/form-data" id="pageEditForm">
<div asp-validation-summary="All" class="text-danger mb-3"></div>
<ul class="nav nav-tabs mb-4" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#basic-tab">Basic Info</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#gallery-tab">Image Gallery</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#team-tab">Team Members</a>
</li>
</ul>
<div class="tab-content">
<!-- Basic Info Tab -->
<div class="tab-pane fade show active" id="basic-tab">
<div class="mb-3">
<label class="form-label">Page Name</label>
<input class="form-control" asp-for="PageName" required />
</div>
<div class="mb-3">
<label class="form-label">Title</label>
<input class="form-control" asp-for="Title" />
</div>
<div class="mb-3">
<label class="form-label">Subtitle</label>
<input class="form-control" asp-for="Subtitle" />
</div>
<div class="mb-3">
<label class="form-label">Content</label>
<textarea class="form-control" asp-for="Content" id="pageContent" rows="15"></textarea>
</div>
<div class="form-check mb-3">
<input asp-for="IsActive" class="form-check-input" type="checkbox" />
<label class="form-check-label">Active</label>
</div>
</div>
<!-- Image Gallery Tab -->
<div class="tab-pane fade" id="gallery-tab">
<div class="mb-3">
<label class="form-label">Image Gallery (Right Sidebar)</label>
<p class="text-muted small">These images will appear on the right side of the About page</p>
<div class="input-group mb-2">
<input type="file" class="form-control" id="galleryImageUpload" accept="image/*" multiple />
<button type="button" class="btn btn-primary" onclick="uploadGalleryImages()">
<i class="bi bi-cloud-upload"></i> Upload Images
</button>
</div>
<small class="text-muted">You can select multiple images at once</small>
</div>
<div id="galleryImagesContainer" class="row g-3">
@if (Model.ImageGallery != null && Model.ImageGallery.Any())
{
for (int i = 0; i < Model.ImageGallery.Count; i++)
{
<div class="col-md-4 gallery-image-item">
<div class="card">
<img src="@Model.ImageGallery[i]" class="card-img-top" style="height: 150px; object-fit: cover;" />
<div class="card-body p-2">
<input type="hidden" name="ImageGallery[@i]" value="@Model.ImageGallery[i]" />
<button type="button" class="btn btn-sm btn-danger w-100" onclick="removeGalleryImage(this)">Remove</button>
</div>
</div>
</div>
}
}
</div>
</div>
<!-- Team Members Tab -->
<div class="tab-pane fade" id="team-tab">
<div class="mb-3">
<button type="button" class="btn btn-primary" onclick="addTeamMember()">
<i class="bi bi-plus-circle"></i> Add Team Member
</button>
</div>
<div id="teamMembersContainer">
@if (Model.TeamMembers != null && Model.TeamMembers.Any())
{
for (int i = 0; i < Model.TeamMembers.Count; i++)
{
<div class="card mb-3 team-member-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">Team Member #@(i + 1)</h6>
<button type="button" class="btn btn-sm btn-danger" onclick="removeTeamMember(this)">Remove</button>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 text-center">
<img src="@(!string.IsNullOrEmpty(Model.TeamMembers[i].PhotoUrl) ? Model.TeamMembers[i].PhotoUrl : "/assets/images/placeholder.jpg")"
class="team-member-preview rounded-circle mb-2"
style="width: 120px; height: 120px; object-fit: cover; border: 3px solid #6B4E9B;" />
<input type="file" class="form-control form-control-sm" accept="image/*" onchange="previewTeamPhoto(this)" />
<input type="hidden" name="TeamMembers[@i].PhotoUrl" value="@Model.TeamMembers[i].PhotoUrl" class="team-photo-url" />
</div>
<div class="col-md-9">
<div class="mb-2">
<label class="form-label">Name</label>
<input type="text" class="form-control" name="TeamMembers[@i].Name" value="@Model.TeamMembers[i].Name" required />
</div>
<div class="mb-2">
<label class="form-label">Role/Position</label>
<input type="text" class="form-control" name="TeamMembers[@i].Role" value="@Model.TeamMembers[i].Role" />
</div>
<div class="mb-2">
<label class="form-label">Bio</label>
<textarea class="form-control" name="TeamMembers[@i].Bio" rows="3">@Model.TeamMembers[i].Bio</textarea>
</div>
</div>
</div>
</div>
</div>
}
}
</div>
</div>
</div>
<div class="mt-4">
<button class="btn btn-primary" type="submit">Save Changes</button>
<a class="btn btn-secondary" href="/admin/pages">Cancel</a>
</div>
</form>
</div>
</div>
@section Scripts {
<script src="https://cdn.ckeditor.com/ckeditor5/40.1.0/classic/ckeditor.js"></script>
<script>
ClassicEditor
.create(document.querySelector('#pageContent'), {
toolbar: {
items: [
'heading', '|',
'bold', 'italic', 'underline', 'strikethrough', '|',
'link', 'blockQuote', '|',
'bulletedList', 'numberedList', '|',
'outdent', 'indent', '|',
'alignment', '|',
'insertTable', '|',
'fontSize', 'fontColor', 'fontBackgroundColor', '|',
'removeFormat', '|',
'undo', 'redo', '|',
'sourceEditing'
],
shouldNotGroupWhenFull: true
},
heading: {
options: [
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
{ model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' },
{ model: 'heading4', view: 'h4', title: 'Heading 4', class: 'ck-heading_heading4' }
]
},
fontSize: {
options: [
'small',
'default',
'big'
]
},
table: {
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells']
},
htmlSupport: {
allow: [
{
name: /.*/,
attributes: true,
classes: true,
styles: true
}
]
}
})
.catch(error => {
console.error(error);
});
// Gallery Image Upload (Multiple)
function uploadGalleryImages() {
const fileInput = document.getElementById('galleryImageUpload');
const files = fileInput.files;
if (files.length === 0) {
alert('Please select at least one image');
return;
}
// Show uploading indicator
const button = event.target;
const originalText = button.innerHTML;
button.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Uploading...';
button.disabled = true;
let uploadedCount = 0;
let failedCount = 0;
// Upload each file
Array.from(files).forEach((file, index) => {
const formData = new FormData();
formData.append('image', file);
fetch('/api/upload/image', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
addGalleryImageToList(data.imageUrl);
uploadedCount++;
} else {
console.error('Upload failed:', data.message);
failedCount++;
}
// Check if all uploads are complete
if (uploadedCount + failedCount === files.length) {
button.innerHTML = originalText;
button.disabled = false;
fileInput.value = '';
if (uploadedCount > 0) {
alert(`Successfully uploaded ${uploadedCount} image(s)${failedCount > 0 ? `, ${failedCount} failed` : ''}`);
} else {
alert('All uploads failed. Please try again.');
}
}
})
.catch(error => {
console.error('Upload error:', error);
failedCount++;
if (uploadedCount + failedCount === files.length) {
button.innerHTML = originalText;
button.disabled = false;
fileInput.value = '';
alert(`Upload completed. ${uploadedCount} succeeded, ${failedCount} failed.`);
}
});
});
}
function addGalleryImageToList(imageUrl) {
const container = document.getElementById('galleryImagesContainer');
const count = container.querySelectorAll('.gallery-image-item').length;
const html = `
<div class="col-md-4 gallery-image-item">
<div class="card">
<img src="${imageUrl}" class="card-img-top" style="height: 150px; object-fit: cover;" />
<div class="card-body p-2">
<input type="hidden" name="ImageGallery[${count}]" value="${imageUrl}" />
<button type="button" class="btn btn-sm btn-danger w-100" onclick="removeGalleryImage(this)">Remove</button>
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeGalleryImage(button) {
const item = button.closest('.gallery-image-item');
item.remove();
reindexGalleryImages();
}
function reindexGalleryImages() {
const items = document.querySelectorAll('.gallery-image-item');
items.forEach((item, index) => {
const input = item.querySelector('input[type="hidden"]');
input.name = `ImageGallery[${index}]`;
});
}
// Team Member Management
let teamMemberIndex = document.querySelectorAll('.team-member-card').length;
function addTeamMember() {
const container = document.getElementById('teamMembersContainer');
const html = `
<div class="card mb-3 team-member-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">Team Member #${teamMemberIndex + 1}</h6>
<button type="button" class="btn btn-sm btn-danger" onclick="removeTeamMember(this)">Remove</button>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 text-center">
<img src="/assets/images/placeholder.jpg"
class="team-member-preview rounded-circle mb-2"
style="width: 120px; height: 120px; object-fit: cover; border: 3px solid #6B4E9B;" />
<input type="file" class="form-control form-control-sm" accept="image/*" onchange="previewTeamPhoto(this)" />
<input type="hidden" name="TeamMembers[${teamMemberIndex}].PhotoUrl" value="" class="team-photo-url" />
</div>
<div class="col-md-9">
<div class="mb-2">
<label class="form-label">Name</label>
<input type="text" class="form-control" name="TeamMembers[${teamMemberIndex}].Name" required />
</div>
<div class="mb-2">
<label class="form-label">Role/Position</label>
<input type="text" class="form-control" name="TeamMembers[${teamMemberIndex}].Role" />
</div>
<div class="mb-2">
<label class="form-label">Bio</label>
<textarea class="form-control" name="TeamMembers[${teamMemberIndex}].Bio" rows="3"></textarea>
</div>
</div>
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
teamMemberIndex++;
}
function removeTeamMember(button) {
const card = button.closest('.team-member-card');
card.remove();
reindexTeamMembers();
}
function reindexTeamMembers() {
const cards = document.querySelectorAll('.team-member-card');
cards.forEach((card, index) => {
card.querySelector('h6').textContent = `Team Member #${index + 1}`;
card.querySelectorAll('input, textarea').forEach(input => {
const name = input.getAttribute('name');
if (name && name.startsWith('TeamMembers[')) {
const newName = name.replace(/TeamMembers\[\d+\]/, `TeamMembers[${index}]`);
input.setAttribute('name', newName);
}
});
});
teamMemberIndex = cards.length;
}
function previewTeamPhoto(input) {
const file = input.files[0];
if (!file) return;
const card = input.closest('.team-member-card');
const preview = card.querySelector('.team-member-preview');
const hiddenInput = card.querySelector('.team-photo-url');
// Validate file type
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
alert('Please select a valid image file (JPG, PNG, GIF, or WebP)');
input.value = '';
return;
}
// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
alert('Image file is too large. Please select an image smaller than 5MB.');
input.value = '';
return;
}
// Add loading border to preview
preview.style.opacity = '0.5';
preview.style.border = '3px solid #ffc107';
// Show preview immediately
const reader = new FileReader();
reader.onload = function(e) {
preview.src = e.target.result;
};
reader.readAsDataURL(file);
// Upload to server
const formData = new FormData();
formData.append('image', file);
console.log('Uploading team member photo...');
fetch('/api/upload/image', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
console.log('Response status:', response.status);
return response.json();
})
.then(data => {
console.log('Upload response:', data);
preview.style.opacity = '1';
if (data.success) {
hiddenInput.value = data.imageUrl;
preview.style.border = '3px solid #28a745';
// Reset border color after 2 seconds
setTimeout(() => {
preview.style.border = '3px solid #6B4E9B';
}, 2000);
console.log('Photo uploaded successfully:', data.imageUrl);
} else {
alert('Upload failed: ' + (data.message || 'Unknown error'));
preview.style.border = '3px solid #dc3545';
input.value = '';
// Reset to placeholder after error
setTimeout(() => {
preview.src = '/assets/images/placeholder.jpg';
preview.style.border = '3px solid #6B4E9B';
}, 2000);
}
})
.catch(error => {
console.error('Upload error:', error);
alert('Upload failed. Please check console for details.');
preview.style.opacity = '1';
preview.style.border = '3px solid #dc3545';
input.value = '';
// Reset to placeholder after error
setTimeout(() => {
preview.src = '/assets/images/placeholder.jpg';
preview.style.border = '3px solid #6B4E9B';
}, 2000);
});
}
</script>
}

View File

@@ -0,0 +1,120 @@
@model SkyArtShop.Models.PortfolioCategory
@{
Layout = "~/Views/Shared/_AdminLayout.cshtml";
ViewData["Title"] = "Create Category";
}
<style>
.form-check-input[type="checkbox"] {
width: 22px;
height: 22px;
cursor: pointer;
border: 2px solid #dee2e6;
border-radius: 4px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-color: white;
transition: all 0.2s ease;
position: relative;
}
.form-check-input[type="checkbox"]:checked {
background-color: #28a745;
border-color: #28a745;
}
.form-check-input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 16px;
font-weight: bold;
line-height: 1;
}
.form-check-input[type="checkbox"]:hover {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
</style>
<div class="card">
<div class="card-body">
<form method="post">
<div asp-validation-summary="All" class="text-danger mb-3"></div>
<div class="mb-3">
<label class="form-label">Name</label>
<input class="form-control" name="Name" value="@Model.Name" />
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea class="form-control" id="categoryDescription" name="Description">@Model.Description</textarea>
</div>
<div class="mb-3">
<label class="form-label">Display Order</label>
<input type="number" class="form-control" name="DisplayOrder" value="@Model.DisplayOrder" />
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="IsActive" @(Model.IsActive ? "checked" : "") />
<label class="form-check-label">Active</label>
</div>
<button class="btn btn-primary" type="submit">Save</button>
<a class="btn btn-secondary" href="/admin/portfolio/categories">Cancel</a>
</form>
</div>
</div>
@section Scripts {
<script src="https://cdn.ckeditor.com/ckeditor5/40.1.0/classic/ckeditor.js"></script>
<script>
let categoryEditor;
ClassicEditor
.create(document.querySelector('#categoryDescription'), {
toolbar: {
items: [
'heading', '|',
'bold', 'italic', 'underline', 'strikethrough', '|',
'link', 'blockQuote', '|',
'bulletedList', 'numberedList', '|',
'outdent', 'indent', '|',
'alignment', '|',
'insertTable', '|',
'fontSize', 'fontColor', 'fontBackgroundColor', '|',
'removeFormat', '|',
'undo', 'redo', '|',
'sourceEditing'
],
shouldNotGroupWhenFull: true
},
heading: {
options: [
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
{ model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' },
{ model: 'heading4', view: 'h4', title: 'Heading 4', class: 'ck-heading_heading4' }
]
},
fontSize: {
options: ['small', 'default', 'big']
},
table: {
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells']
},
htmlSupport: {
allow: [{ name: /.*/, attributes: true, classes: true, styles: true }]
}
})
.then(editor => {
categoryEditor = editor;
document.querySelector('form').addEventListener('submit', function(e) {
document.querySelector('#categoryDescription').value = categoryEditor.getData();
});
})
.catch(error => { console.error(error); });
</script>
}

View File

@@ -0,0 +1,120 @@
@model SkyArtShop.Models.PortfolioCategory
@{
Layout = "~/Views/Shared/_AdminLayout.cshtml";
ViewData["Title"] = "Edit Category";
}
<style>
.form-check-input[type="checkbox"] {
width: 22px;
height: 22px;
cursor: pointer;
border: 2px solid #dee2e6;
border-radius: 4px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-color: white;
transition: all 0.2s ease;
position: relative;
}
.form-check-input[type="checkbox"]:checked {
background-color: #28a745;
border-color: #28a745;
}
.form-check-input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 16px;
font-weight: bold;
line-height: 1;
}
.form-check-input[type="checkbox"]:hover {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
</style>
<div class="card">
<div class="card-body">
<form method="post">
<div asp-validation-summary="All" class="text-danger mb-3"></div>
<div class="mb-3">
<label class="form-label">Name</label>
<input class="form-control" name="Name" value="@Model.Name" />
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea class="form-control" id="categoryDescription" name="Description">@Model.Description</textarea>
</div>
<div class="mb-3">
<label class="form-label">Display Order</label>
<input type="number" class="form-control" name="DisplayOrder" value="@Model.DisplayOrder" />
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="IsActive" @(Model.IsActive ? "checked" : "") />
<label class="form-check-label">Active</label>
</div>
<button class="btn btn-primary" type="submit">Save</button>
<a class="btn btn-secondary" href="/admin/portfolio/categories">Cancel</a>
</form>
</div>
</div>
@section Scripts {
<script src="https://cdn.ckeditor.com/ckeditor5/40.1.0/classic/ckeditor.js"></script>
<script>
let categoryEditor;
ClassicEditor
.create(document.querySelector('#categoryDescription'), {
toolbar: {
items: [
'heading', '|',
'bold', 'italic', 'underline', 'strikethrough', '|',
'link', 'blockQuote', '|',
'bulletedList', 'numberedList', '|',
'outdent', 'indent', '|',
'alignment', '|',
'insertTable', '|',
'fontSize', 'fontColor', 'fontBackgroundColor', '|',
'removeFormat', '|',
'undo', 'redo', '|',
'sourceEditing'
],
shouldNotGroupWhenFull: true
},
heading: {
options: [
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
{ model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' },
{ model: 'heading4', view: 'h4', title: 'Heading 4', class: 'ck-heading_heading4' }
]
},
fontSize: {
options: ['small', 'default', 'big']
},
table: {
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells']
},
htmlSupport: {
allow: [{ name: /.*/, attributes: true, classes: true, styles: true }]
}
})
.then(editor => {
categoryEditor = editor;
document.querySelector('form').addEventListener('submit', function(e) {
document.querySelector('#categoryDescription').value = categoryEditor.getData();
});
})
.catch(error => { console.error(error); });
</script>
}

582
Views/AdminProducts/Form.cshtml Executable file
View File

@@ -0,0 +1,582 @@
@model Product
@{
ViewData["Title"] = Model?.Id == null ? "Create Product" : "Edit Product";
Layout = "_AdminLayout";
}
<style>
.form-check-input[type="checkbox"] {
width: 22px;
height: 22px;
cursor: pointer;
border: 2px solid #dee2e6;
border-radius: 4px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-color: white;
transition: all 0.2s ease;
position: relative;
}
.form-check-input[type="checkbox"]:checked {
background-color: #28a745;
border-color: #28a745;
}
.form-check-input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 16px;
font-weight: bold;
line-height: 1;
}
.form-check-input[type="checkbox"]:hover {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
</style>
@await Html.PartialAsync("_ImagePickerModal")
@await Html.PartialAsync("_VariantManagerModal")
<div class="card">
<div class="card-header">
<h5 class="mb-0">@ViewData["Title"]</h5>
</div>
<div class="card-body">
<form method="post" action="/admin/products/@(Model?.Id == null ? "create" : $"edit/{Model.Id}")">
<div asp-validation-summary="All" class="text-danger mb-3"></div>
<input type="hidden" name="Id" value="@Model?.Id" />
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="Name" class="form-label">Product Name *</label>
<input type="text" class="form-control" id="Name" name="Name" value="@Model?.Name" required>
</div>
<div class="mb-3">
<label for="SKU" class="form-label">SKU Code</label>
<input type="text" class="form-control" id="SKU" name="SKU" value="@Model?.SKU"
placeholder="e.g., AB-001, WASH-2024-01">
<small class="form-text text-muted">Unique product identifier (leave empty to
auto-generate)</small>
</div>
<div class="mb-3">
<label for="ShortDescription" class="form-label">Short Description</label>
<textarea class="form-control" id="ShortDescription" name="ShortDescription" rows="3"
placeholder="Brief product description (shown in listings)">@Model?.ShortDescription</textarea>
</div>
<div class="mb-3">
<label for="Description" class="form-label">Full Description</label>
<textarea class="form-control" id="Description" name="Description"
rows="10">@Model?.Description</textarea>
</div>
<div class="row">
<div class="col-md-3">
<div class="mb-3">
<label for="Price" class="form-label">Selling Price *</label>
<input type="number" step="0.01" class="form-control" id="Price" name="Price"
value="@Model?.Price" required>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="CostPrice" class="form-label">Cost Price</label>
<input type="number" step="0.01" class="form-control" id="CostPrice" name="CostPrice"
value="@Model?.CostPrice" placeholder="Your cost">
<small class="form-text text-muted">For profit margin calculation</small>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="StockQuantity" class="form-label">Stock Quantity</label>
<input type="number" class="form-control" id="StockQuantity" name="StockQuantity"
value="@(Model?.StockQuantity ?? 0)">
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="Category" class="form-label">Category</label>
<input type="text" class="form-control" id="Category" name="Category"
value="@Model?.Category" placeholder="e.g., Washi Tape, Stickers">
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Product Color Variants</label>
<div class="border rounded p-3 bg-light">
<div class="d-flex justify-content-between align-items-center mb-2">
<div>
<strong>Color Variant System (NEW)</strong>
<p class="text-muted small mb-0">Link specific images to color variants for better customer experience</p>
</div>
<button type="button" class="btn btn-primary btn-sm" onclick="openVariantManager()">
<i class="bi bi-palette"></i> Manage Variants
</button>
</div>
<div id="variantSummary" class="mt-2">
<span class="badge bg-secondary">No variants</span>
</div>
</div>
<input type="hidden" id="productVariantsData" name="ProductVariantsJson" value="" />
</div>
<!-- Legacy color picker removed - Use "Manage Variants" button above -->
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Product Images</label>
<div class="border rounded p-3" style="min-height: 200px;">
<div id="imageGallery" class="d-flex flex-wrap gap-2" style="position: relative;">
@if (Model?.Images != null && Model.Images.Any())
{
@for (int i = 0; i < Model.Images.Count; i++)
{
<div class="image-item position-relative" draggable="true" style="width: 80px; height: 80px; cursor: move;" data-image-url="@Model.Images[i]">
<img src="@Model.Images[i]" class="img-thumbnail" style="width: 100%; height: 100%; object-fit: cover; pointer-events: none;">
<button type="button" class="btn btn-danger btn-sm position-absolute top-0 end-0"
style="padding: 2px 6px; font-size: 0.7rem; z-index: 10;"
onclick="removeImageElement(this)">
<i class="bi bi-x"></i>
</button>
@if (i == 0)
{
<span class="badge bg-primary position-absolute bottom-0 start-0 m-1" style="font-size: 0.65rem;">Main</span>
}
<input type="hidden" name="Images" value="@Model.Images[i]">
</div>
}
}
</div>
<div id="uploadPlaceholder" class="text-center"
style="display: @(Model?.Images == null || !Model.Images.Any() ? "block" : "none"); padding: 40px 0;">
<i class="bi bi-image" style="font-size: 48px; color: #ccc;"></i>
<p class="text-muted mt-2">No images uploaded</p>
</div>
</div>
<input type="hidden" id="ImageUrl" name="ImageUrl" value="@Model?.ImageUrl">
<button type="button" class="btn btn-primary btn-sm mt-2 w-100"
onclick="openImagePicker(handleImagePickerSelection, 'multiple')">
<i class="bi bi-images"></i> Select/Upload Images
</button>
<small class="text-muted d-block mt-1">Drag images to reorder. First image is the main display image.</small>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> <strong>Product Detail Page:</strong>
<ul class="mb-0 mt-2" style="font-size:0.9rem;">
<li>Main image and additional images will display in gallery</li>
<li>SKU, price, stock, and color show in product info</li>
<li>Short description appears below buttons</li>
<li>Full description displays in expandable section</li>
<li>Related products suggested based on category & views</li>
</ul>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Product Settings</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="IsActive" name="IsActive" value="true"
@(Model?.IsActive != false ? "checked" : "")>
<label class="form-check-label" for="IsActive">
<strong>Active</strong> - Product visible in shop
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="IsFeatured" name="IsFeatured" value="true"
@(Model?.IsFeatured == true ? "checked" : "")>
<label class="form-check-label" for="IsFeatured">
<strong>Featured</strong> - Show in featured products section
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="IsTopSeller" name="IsTopSeller" value="true"
@(Model?.IsTopSeller == true ? "checked" : "")>
<label class="form-check-label" for="IsTopSeller">
<strong>Top Seller</strong> - Show in top sellers section
</label>
</div>
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-between">
<a href="/admin/products" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Save Product
</button>
</div>
</form>
</div>
</div>
@section Scripts {
<script src="https://cdn.ckeditor.com/ckeditor5/40.1.0/classic/ckeditor.js"></script>
<script>
let descriptionEditor;
// Initialize CKEditor for Description
ClassicEditor
.create(document.querySelector('#Description'), {
toolbar: [
'heading', '|',
'bold', 'italic', '|',
'link', 'bulletedList', 'numberedList', '|',
'indent', 'outdent', '|',
'blockQuote', 'insertTable', '|',
'undo', 'redo'
],
heading: {
options: [
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
{ model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' }
]
}
})
.then(editor => {
descriptionEditor = editor;
// Sync CKEditor data before form submission
document.querySelector('form').addEventListener('submit', function (e) {
document.querySelector('#Description').value = descriptionEditor.getData();
});
})
.catch(error => {
console.error(error);
});
let imageIndex = @(Model?.Images?.Count ?? 0);
// Handle Image Picker Selection
function handleImagePickerSelection(selectedUrls) {
const gallery = document.getElementById('imageGallery');
const placeholder = document.getElementById('uploadPlaceholder');
if (selectedUrls.length > 0) {
placeholder.style.display = 'none';
selectedUrls.forEach(imageUrl => {
const imageDiv = document.createElement('div');
imageDiv.className = 'image-item position-relative';
imageDiv.draggable = true;
imageDiv.style.width = '80px';
imageDiv.style.height = '80px';
imageDiv.style.cursor = 'move';
imageDiv.setAttribute('data-image-url', imageUrl);
const isFirstImage = gallery.querySelectorAll('.image-item').length === 0;
const mainBadge = isFirstImage ? '<span class="badge bg-primary position-absolute bottom-0 start-0 m-1" style="font-size: 0.65rem; z-index: 10;">Main</span>' : '';
imageDiv.innerHTML = `
<img src="${imageUrl}" class="img-thumbnail" style="width: 100%; height: 100%; object-fit: cover; pointer-events: none;">
<button type="button" class="btn btn-danger btn-sm position-absolute top-0 end-0"
style="padding: 2px 6px; font-size: 0.7rem; z-index: 10;"
onclick="removeImageElement(this)">
<i class="bi bi-x"></i>
</button>
${mainBadge}
<input type="hidden" name="Images" value="${imageUrl}">
`;
gallery.appendChild(imageDiv);
imageIndex++;
// Set first image as main ImageUrl
if (gallery.children.length === 1 || !document.getElementById('ImageUrl').value) {
document.getElementById('ImageUrl').value = imageUrl;
}
});
// Re-initialize drag and drop for new elements
setTimeout(() => initializeDragAndDrop(), 100);
}
}
async function handleImageUpload(event) {
const files = event.target.files;
const gallery = document.getElementById('imageGallery');
const placeholder = document.getElementById('uploadPlaceholder');
if (files.length > 0) {
placeholder.style.display = 'none';
for (let i = 0; i < files.length; i++) {
const file = files[i];
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/admin/upload/image', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
const imageUrl = result.url;
const imageDiv = document.createElement('div');
imageDiv.className = 'image-item position-relative';
imageDiv.draggable = true;
imageDiv.style.width = '80px';
imageDiv.style.height = '80px';
imageDiv.style.cursor = 'move';
imageDiv.setAttribute('data-image-url', imageUrl);
const isFirstImage = gallery.querySelectorAll('.image-item').length === 0;
const mainBadge = isFirstImage ? '<span class="badge bg-primary position-absolute bottom-0 start-0 m-1" style="font-size: 0.65rem; z-index: 10;">Main</span>' : '';
imageDiv.innerHTML = `
<img src="${imageUrl}" class="img-thumbnail" style="width: 100%; height: 100%; object-fit: cover; pointer-events: none;">
<button type="button" class="btn btn-danger btn-sm position-absolute top-0 end-0"
style="padding: 2px 6px; font-size: 0.7rem; z-index: 10;"
onclick="removeImageElement(this)">
<i class="bi bi-x"></i>
</button>
${mainBadge}
<input type="hidden" name="Images" value="${imageUrl}">
`;
gallery.appendChild(imageDiv);
imageIndex++;
// Set first image as main ImageUrl
if (gallery.children.length === 1 || !document.getElementById('ImageUrl').value) {
document.getElementById('ImageUrl').value = imageUrl;
}
} else {
alert('Error uploading image: ' + result.message);
}
} catch (error) {
alert('Error uploading image');
}
}
}
// Reset file input
event.target.value = '';
}
function removeImageElement(button) {
const imageDiv = button.closest('.position-relative');
const gallery = document.getElementById('imageGallery');
const placeholder = document.getElementById('uploadPlaceholder');
imageDiv.remove();
// Show placeholder if no images left
if (gallery.children.length === 0) {
placeholder.style.display = 'block';
document.getElementById('ImageUrl').value = '';
} else {
// Update main ImageUrl to first image if removed image was main
const firstImage = gallery.querySelector('img');
if (firstImage) {
const currentMain = document.getElementById('ImageUrl').value;
const allImages = Array.from(gallery.querySelectorAll('input[type="hidden"]')).map(input => input.value);
if (!allImages.includes(currentMain)) {
document.getElementById('ImageUrl').value = allImages[0];
}
}
}
}
function removeImage(index) {
if (confirm('Remove this image?')) {
const gallery = document.getElementById('imageGallery');
const imageDiv = gallery.children[index];
removeImageElement(imageDiv.querySelector('button'));
}
}
// Drag and Drop Functionality
let draggedElement = null;
document.addEventListener('DOMContentLoaded', function() {
initializeDragAndDrop();
});
function initializeDragAndDrop() {
const gallery = document.getElementById('imageGallery');
gallery.addEventListener('dragstart', function(e) {
if (e.target.classList.contains('image-item')) {
draggedElement = e.target;
e.target.classList.add('dragging');
e.target.style.opacity = '0.5';
}
});
gallery.addEventListener('dragend', function(e) {
if (e.target.classList.contains('image-item')) {
e.target.classList.remove('dragging');
e.target.style.opacity = '1';
updateMainBadge();
updateImageUrl();
}
});
gallery.addEventListener('dragover', function(e) {
e.preventDefault();
const afterElement = getDragAfterElement(gallery, e.clientX, e.clientY);
if (draggedElement) {
if (afterElement == null) {
gallery.appendChild(draggedElement);
} else {
gallery.insertBefore(draggedElement, afterElement);
}
}
});
gallery.addEventListener('drop', function(e) {
e.preventDefault();
});
}
function getDragAfterElement(container, x, y) {
const draggableElements = [...container.querySelectorAll('.image-item:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const centerX = box.left + box.width / 2;
const centerY = box.top + box.height / 2;
// Calculate distance from mouse to center of element
const offsetX = x - centerX;
const offsetY = y - centerY;
// For horizontal layout, primarily use X offset
if (offsetX < 0 && (closest.offset === undefined || offsetX > closest.offset)) {
return { offset: offsetX, element: child };
} else {
return closest;
}
}, { offset: undefined, element: null }).element;
}
function updateMainBadge() {
const gallery = document.getElementById('imageGallery');
const images = gallery.querySelectorAll('.image-item');
images.forEach((item, index) => {
// Remove existing main badge
const existingBadge = item.querySelector('.badge');
if (existingBadge) {
existingBadge.remove();
}
// Add main badge to first image
if (index === 0) {
const badge = document.createElement('span');
badge.className = 'badge bg-primary position-absolute bottom-0 start-0 m-1';
badge.style.fontSize = '0.65rem';
badge.textContent = 'Main';
item.appendChild(badge);
}
});
}
function updateImageUrl() {
const gallery = document.getElementById('imageGallery');
const firstImage = gallery.querySelector('.image-item img');
if (firstImage) {
document.getElementById('ImageUrl').value = firstImage.src;
}
}
// Update drag functionality when new images are added
const originalHandleImageUpload = handleImageUpload;
handleImageUpload = async function(event) {
await originalHandleImageUpload(event);
setTimeout(() => {
const newImages = document.querySelectorAll('.image-item');
newImages.forEach(item => {
if (!item.draggable) {
item.draggable = true;
item.style.cursor = 'move';
}
});
}, 100);
};
// Color selection toggle
function toggleColorSelection(label) {
const checkbox = label.previousElementSibling;
checkbox.checked = !checkbox.checked;
// Update visual state
if (checkbox.checked) {
const color = checkbox.value;
const checkIcon = color === "White" || color === "Yellow" ? "black" : "white";
label.innerHTML = `<i class="bi bi-check-lg" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: ${checkIcon}; font-size: 1.5rem; font-weight: bold;"></i>`;
label.style.transform = "scale(1.1)";
} else {
label.innerHTML = "";
label.style.transform = "scale(1)";
}
}
// Open Variant Manager
function openVariantManager() {
// Get all product images
const productImages = Array.from(document.querySelectorAll('#imageGallery .image-item img'))
.map(img => img.src);
// Load existing variants
const variantsJson = document.getElementById('productVariantsData').value;
const existingVariants = variantsJson ? JSON.parse(variantsJson) : [];
// Initialize variant manager
initVariantManager(existingVariants, productImages);
// Show modal
const modal = new bootstrap.Modal(document.getElementById('variantManagerModal'));
modal.show();
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
// Load existing variants if editing
@if (Model?.Variants != null && Model.Variants.Any())
{
<text>
const existingVariants = @Html.Raw(Json.Serialize(Model.Variants));
document.getElementById('productVariantsData').value = JSON.stringify(existingVariants);
productVariants = existingVariants;
updateVariantSummary();
</text>
}
// Debug form submission
const form = document.querySelector('form');
form.addEventListener('submit', function(e) {
const variantData = document.getElementById('productVariantsData').value;
console.log('=== FORM SUBMISSION DEBUG ===');
console.log('ProductVariantsJson field value:', variantData);
console.log('Is empty?', !variantData || variantData === '');
console.log('productVariants array:', productVariants);
console.log('============================');
if (!variantData || variantData === '' || variantData === '[]') {
console.warn('WARNING: No variant data in form! Did you click "Apply Changes" in Variant Manager?');
}
});
});
</script>
}

109
Views/AdminProducts/Index.cshtml Executable file
View File

@@ -0,0 +1,109 @@
@model List<Product>
@{
ViewData["Title"] = "Manage Products";
Layout = "_AdminLayout";
}
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">All Products (@Model.Count)</h5>
<a href="/admin/products/create" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add New Product
</a>
</div>
<div class="card">
<div class="card-body">
@if (Model.Any())
{
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Image</th>
<th>Name</th>
<th>Category</th>
<th>Price</th>
<th>Stock</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var product in Model.OrderByDescending(p => p.CreatedAt))
{
<tr>
<td>
@if (!string.IsNullOrEmpty(product.ImageUrl))
{
<img src="@product.ImageUrl" alt="@product.Name" style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;">
}
else
{
<div style="width: 50px; height: 50px; background: #e0e0e0; border-radius: 4px;"></div>
}
</td>
<td>
<strong>@product.Name</strong>
@if (product.IsFeatured)
{
<span class="badge bg-warning text-dark ms-1">Featured</span>
}
@if (product.IsTopSeller)
{
<span class="badge bg-success ms-1">Top Seller</span>
}
</td>
<td>@product.Category</td>
<td>$@product.Price.ToString("F2")</td>
<td>@product.StockQuantity</td>
<td>
@if (product.IsActive)
{
<span class="badge bg-success"><i class="bi bi-check-circle-fill"></i> Active</span>
}
else
{
<span class="badge bg-danger"><i class="bi bi-x-circle-fill"></i> Inactive</span>
}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="/admin/products/edit/@product.Id" class="btn btn-outline-primary">
<i class="bi bi-pencil"></i>
</a>
<button onclick="deleteProduct('@product.Id', '@product.Name')" class="btn btn-outline-danger">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<p class="text-center text-muted my-5">No products found. Create your first product!</p>
}
</div>
</div>
@section Scripts {
<script>
function deleteProduct(id, name) {
if (confirm(`Are you sure you want to delete "${name}"?`)) {
fetch(`/admin/products/delete/${id}`, {
method: 'POST'
})
.then(response => {
if (response.ok) {
location.reload();
} else {
alert('Error deleting product');
}
});
}
}
</script>
}

612
Views/AdminUpload/Index.cshtml Executable file
View File

@@ -0,0 +1,612 @@
@model List<string>
@{
Layout = "~/Views/Shared/_AdminLayout.cshtml";
ViewData["Title"] = "Media Upload";
}
<div class="mb-4">
<h2>Media Upload</h2>
<p class="text-muted">Upload and manage your images</p>
</div>
<div class="card mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="card-title mb-0">Upload New Images</h5>
<button type="button" class="btn btn-primary" onclick="showUploadModal()">
<i class="bi bi-cloud-upload"></i> Choose Files
</button>
</div>
<div id="uploadResult" class="mt-3"></div>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="card-title mb-0">Media Library (@Model.Count images)</h5>
<div>
<button type="button" class="btn btn-sm btn-primary me-2" onclick="showCreateFolderModal()">
<i class="bi bi-folder-plus"></i> New Folder
</button>
</div>
</div>
<!-- Bulk Actions Toolbar -->
<div id="bulkActionsBar" class="alert alert-info d-none mb-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<input type="checkbox" class="form-check-input me-2" id="selectAllImages" onchange="toggleSelectAll(this)">
<strong><span id="selectedCount">0</span> images selected</strong>
</div>
<div>
<button type="button" class="btn btn-sm btn-danger" onclick="deleteSelectedImages()">
<i class="bi bi-trash"></i> Delete Selected
</button>
<button type="button" class="btn btn-sm btn-secondary" onclick="clearSelection()">
<i class="bi bi-x-lg"></i> Clear Selection
</button>
</div>
</div>
</div>
<!-- Folders Section -->
<div id="foldersSection" class="mb-4">
<h6 class="text-muted">Folders</h6>
<div id="foldersList" class="row g-3 mb-3">
<div class="col-12 text-center text-muted">
<small>Loading folders...</small>
</div>
</div>
</div>
<!-- Images Section -->
<h6 class="text-muted">All Images</h6>
@if (Model.Any())
{
<div class="row g-3" id="imagesGrid">
@foreach (var image in Model)
{
<div class="col-md-3 image-item" data-image="@image">
<div class="card position-relative">
<div class="position-absolute top-0 start-0 p-2">
<input type="checkbox" class="form-check-input image-checkbox" data-image="@image" onchange="updateSelection()">
</div>
<img src="@image" class="card-img-top" alt="Uploaded image" style="height: 200px; object-fit: cover; cursor: pointer;" onclick="toggleImageSelection(this)">
<div class="card-body p-2">
<div class="input-group input-group-sm">
<input type="text" class="form-control" value="@image" readonly onclick="this.select()">
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('@image')">
<i class="bi bi-clipboard"></i>
</button>
</div>
<button class="btn btn-sm btn-danger w-100 mt-2" onclick="deleteImage('@image', this)">
<i class="bi bi-trash"></i> Delete
</button>
</div>
</div>
</div>
}
</div>
}
else
{
<p class="text-muted">No images uploaded yet.</p>
}
</div>
</div>
@section Scripts {
<script>
// Load folders on page load
document.addEventListener('DOMContentLoaded', function() {
loadFolders();
});
function loadFolders() {
fetch('/admin/upload/list-folders')
.then(response => response.json())
.then(folders => {
const foldersList = document.getElementById('foldersList');
if (folders.length === 0) {
foldersList.innerHTML = '<div class="col-12 text-center text-muted"><small>No folders yet</small></div>';
return;
}
foldersList.innerHTML = '';
folders.forEach(folder => {
const col = document.createElement('div');
col.className = 'col-md-3';
col.innerHTML = `
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-folder-fill text-warning" style="font-size: 2rem;"></i>
<div class="ms-2">
<h6 class="mb-0">${folder.name}</h6>
<small class="text-muted">${folder.fileCount} files</small>
</div>
</div>
<button class="btn btn-sm btn-danger w-100" onclick="deleteFolder('${folder.path}', this)">
<i class="bi bi-trash"></i> Delete Folder
</button>
</div>
</div>
`;
foldersList.appendChild(col);
});
})
.catch(error => {
console.error('Error loading folders:', error);
});
}
function showCreateFolderModal() {
const modal = new bootstrap.Modal(document.getElementById('createFolderModal'));
document.getElementById('newFolderName').value = '';
modal.show();
}
function createFolder() {
const folderName = document.getElementById('newFolderName').value.trim();
if (!folderName) {
alert('Please enter a folder name');
return;
}
fetch('/admin/upload/create-folder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(folderName)
})
.then(response => response.json())
.then(result => {
if (result.success) {
bootstrap.Modal.getInstance(document.getElementById('createFolderModal')).hide();
loadFolders();
} else {
alert('Failed to create folder: ' + result.message);
}
})
.catch(error => {
alert('Failed to create folder: ' + error);
});
}
let deleteFolderTarget = null;
let deleteFolderButton = null;
function deleteFolder(folderPath, button) {
deleteFolderTarget = folderPath;
deleteFolderButton = button;
const modal = new bootstrap.Modal(document.getElementById('deleteFolderConfirmModal'));
document.getElementById('deleteFolderName').textContent = folderPath;
modal.show();
}
function confirmDeleteFolder() {
if (!deleteFolderTarget) return;
// Show loading state
const modal = document.getElementById('deleteFolderConfirmModal');
const modalBody = modal.querySelector('.modal-body');
const originalContent = modalBody.innerHTML;
const confirmBtn = modal.querySelector('.btn-danger');
confirmBtn.disabled = true;
confirmBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Deleting...';
fetch('/admin/upload/delete-folder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(deleteFolderTarget),
credentials: 'same-origin',
redirect: 'manual'
})
.then(response => {
// Check if redirected to login
if (response.type === 'opaqueredirect' || response.status === 302) {
throw new Error('Session expired. Please refresh the page and log in again.');
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(result => {
if (result.success) {
modalBody.innerHTML = `
<div class="alert alert-success mb-0">
<i class="bi bi-check-circle-fill"></i> Folder deleted successfully!
</div>
`;
setTimeout(() => {
bootstrap.Modal.getInstance(modal).hide();
loadFolders();
modalBody.innerHTML = originalContent;
confirmBtn.disabled = false;
confirmBtn.innerHTML = '<i class="bi bi-trash"></i> Yes, Delete Folder';
}, 1500);
} else {
modalBody.innerHTML = `
<div class="alert alert-danger mb-0">
<i class="bi bi-x-circle-fill"></i> Delete failed: ${result.message}
</div>
`;
confirmBtn.disabled = false;
confirmBtn.innerHTML = '<i class="bi bi-trash"></i> Try Again';
}
})
.catch(error => {
modalBody.innerHTML = `
<div class="alert alert-danger mb-0">
<i class="bi bi-x-circle-fill"></i> Delete failed: ${error.message || error}
</div>
`;
confirmBtn.disabled = false;
confirmBtn.innerHTML = '<i class="bi bi-trash"></i> Try Again';
});
}
let selectedFiles = [];
function showUploadModal() {
selectedFiles = [];
document.getElementById('filePreview').innerHTML = '<p class="text-muted text-center">No files selected</p>';
document.getElementById('uploadBtn').disabled = true;
const modal = new bootstrap.Modal(document.getElementById('uploadModal'));
modal.show();
}
function selectFiles() {
document.getElementById('hiddenFileInput').click();
}
function handleFileSelection(input) {
const files = Array.from(input.files);
selectedFiles = files;
displayFilePreview(files);
document.getElementById('uploadBtn').disabled = files.length === 0;
}
function displayFilePreview(files) {
const preview = document.getElementById('filePreview');
if (files.length === 0) {
preview.innerHTML = '<p class="text-muted text-center">No files selected</p>';
return;
}
preview.innerHTML = `
<div class="alert alert-info mb-3">
<i class="bi bi-images"></i> <strong>${files.length}</strong> file(s) selected
</div>
<div class="row g-2">
${files.map((file, index) => `
<div class="col-4">
<div class="card">
<img src="${URL.createObjectURL(file)}" class="card-img-top" style="height: 100px; object-fit: cover;">
<div class="card-body p-1">
<small class="text-truncate d-block">${file.name}</small>
</div>
</div>
</div>
`).join('')}
</div>
`;
}
function uploadSelectedFiles() {
if (selectedFiles.length === 0) return;
const formData = new FormData();
selectedFiles.forEach(file => formData.append('files', file));
const modal = document.getElementById('uploadModal');
const modalBody = modal.querySelector('.modal-body');
const originalContent = modalBody.innerHTML;
const uploadBtn = document.getElementById('uploadBtn');
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Uploading...';
fetch('/admin/upload/multiple', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(result => {
if (result.success) {
modalBody.innerHTML = `
<div class="alert alert-success">
<i class="bi bi-check-circle-fill"></i> ${selectedFiles.length} image(s) uploaded successfully!
</div>
`;
setTimeout(() => {
location.reload();
}, 1500);
} else {
modalBody.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-x-circle-fill"></i> Upload failed: ${result.message}
</div>
`;
uploadBtn.disabled = false;
uploadBtn.innerHTML = '<i class="bi bi-upload"></i> Try Again';
}
})
.catch(error => {
modalBody.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-x-circle-fill"></i> Upload failed: ${error}
</div>
`;
uploadBtn.disabled = false;
uploadBtn.innerHTML = '<i class="bi bi-upload"></i> Try Again';
});
}
// Multi-select functions
function toggleImageSelection(img) {
const checkbox = img.closest('.card').querySelector('.image-checkbox');
checkbox.checked = !checkbox.checked;
updateSelection();
}
function updateSelection() {
const checkboxes = document.querySelectorAll('.image-checkbox:checked');
const count = checkboxes.length;
document.getElementById('selectedCount').textContent = count;
if (count > 0) {
document.getElementById('bulkActionsBar').classList.remove('d-none');
} else {
document.getElementById('bulkActionsBar').classList.add('d-none');
}
// Update select all checkbox
const allCheckboxes = document.querySelectorAll('.image-checkbox');
document.getElementById('selectAllImages').checked = count === allCheckboxes.length && count > 0;
}
function toggleSelectAll(checkbox) {
document.querySelectorAll('.image-checkbox').forEach(cb => {
cb.checked = checkbox.checked;
});
updateSelection();
}
function clearSelection() {
document.querySelectorAll('.image-checkbox').forEach(cb => {
cb.checked = false;
});
updateSelection();
}
function deleteSelectedImages() {
const selected = Array.from(document.querySelectorAll('.image-checkbox:checked')).map(cb => cb.dataset.image);
if (selected.length === 0) return;
const modal = new bootstrap.Modal(document.getElementById('bulkDeleteModal'));
document.getElementById('bulkDeleteCount').textContent = selected.length;
modal.show();
}
function confirmBulkDelete() {
const selected = Array.from(document.querySelectorAll('.image-checkbox:checked')).map(cb => cb.dataset.image);
const modal = document.getElementById('bulkDeleteModal');
const modalBody = modal.querySelector('.modal-body');
const originalContent = modalBody.innerHTML;
const confirmBtn = modal.querySelector('.btn-danger');
confirmBtn.disabled = true;
confirmBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Deleting...';
Promise.all(selected.map(imageUrl =>
fetch('/admin/upload/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(imageUrl)
}).then(r => r.json())
))
.then(results => {
const successCount = results.filter(r => r.success).length;
modalBody.innerHTML = `
<div class="alert alert-success">
<i class="bi bi-check-circle-fill"></i> ${successCount} of ${selected.length} image(s) deleted successfully!
</div>
`;
setTimeout(() => {
location.reload();
}, 1500);
})
.catch(error => {
modalBody.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-x-circle-fill"></i> Bulk delete failed: ${error}
</div>
`;
confirmBtn.disabled = false;
confirmBtn.innerHTML = '<i class="bi bi-trash"></i> Try Again';
});
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
alert('URL copied to clipboard!');
});
}
let deleteTarget = null;
let deleteButton = null;
function deleteImage(imageUrl, button) {
deleteTarget = imageUrl;
deleteButton = button;
// Show Bootstrap confirmation modal
const modal = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
document.getElementById('deleteItemName').textContent = imageUrl.split('/').pop();
modal.show();
}
function confirmDelete() {
if (!deleteTarget) return;
// Show loading state
const modal = document.getElementById('deleteConfirmModal');
const modalBody = modal.querySelector('.modal-body');
const originalContent = modalBody.innerHTML;
const confirmBtn = modal.querySelector('.btn-danger');
confirmBtn.disabled = true;
confirmBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Deleting...';
fetch('/admin/upload/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(deleteTarget),
credentials: 'same-origin',
redirect: 'manual'
})
.then(response => {
// Check if redirected to login
if (response.type === 'opaqueredirect' || response.status === 302) {
throw new Error('Session expired. Please refresh the page and log in again.');
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(result => {
if (result.success) {
modalBody.innerHTML = `
<div class="alert alert-success mb-0">
<i class="bi bi-check-circle-fill"></i> Image deleted successfully!
</div>
`;
deleteButton.closest('.col-md-3').remove();
setTimeout(() => {
bootstrap.Modal.getInstance(modal).hide();
modalBody.innerHTML = originalContent;
confirmBtn.disabled = false;
confirmBtn.innerHTML = '<i class="bi bi-trash"></i> Yes, Delete';
}, 1500);
} else {
modalBody.innerHTML = `
<div class="alert alert-danger mb-0">
<i class="bi bi-x-circle-fill"></i> Delete failed: ${result.message}
</div>
`;
confirmBtn.disabled = false;
confirmBtn.innerHTML = '<i class="bi bi-trash"></i> Try Again';
}
})
.catch(error => {
console.error('Delete error:', error);
modalBody.innerHTML = `
<div class="alert alert-danger mb-0">
<i class="bi bi-x-circle-fill"></i> Delete failed: ${error.message || 'Network error'}<br>
<small class="text-muted">Check browser console for details</small>
</div>
`;
confirmBtn.disabled = false;
confirmBtn.innerHTML = '<i class="bi bi-trash"></i> Try Again';
});
}
</script>
<!-- Delete Image Confirmation Modal -->
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteConfirmLabel">
<i class="bi bi-exclamation-triangle-fill"></i> Confirm Deletion
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="mb-2">Are you sure you want to delete this image?</p>
<p class="text-muted mb-0"><strong id="deleteItemName"></strong></p>
<div class="alert alert-warning mt-3 mb-0">
<i class="bi bi-exclamation-circle"></i> This action cannot be undone.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-lg"></i> Cancel
</button>
<button type="button" class="btn btn-danger" onclick="confirmDelete()">
<i class="bi bi-trash"></i> Yes, Delete
</button>
</div>
</div>
</div>
</div>
<!-- Delete Folder Confirmation Modal -->
<div class="modal fade" id="deleteFolderConfirmModal" tabindex="-1" aria-labelledby="deleteFolderConfirmLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteFolderConfirmLabel">
<i class="bi bi-exclamation-triangle-fill"></i> Delete Folder
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="mb-2">Are you sure you want to delete this folder and all its contents?</p>
<p class="text-muted mb-0"><strong id="deleteFolderName"></strong></p>
<div class="alert alert-danger mt-3 mb-0">
<i class="bi bi-exclamation-triangle-fill"></i> <strong>Warning:</strong> This will permanently delete the folder and all images inside it. This action cannot be undone.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-lg"></i> Cancel
</button>
<button type="button" class="btn btn-danger" onclick="confirmDeleteFolder()">
<i class="bi bi-trash"></i> Yes, Delete Folder
</button>
</div>
</div>
</div>
</div>
<!-- Create Folder Modal -->
<div class="modal fade" id="createFolderModal" tabindex="-1" aria-labelledby="createFolderLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="createFolderLabel">
<i class="bi bi-folder-plus"></i> Create New Folder
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="newFolderName" class="form-label">Folder Name</label>
<input type="text" class="form-control" id="newFolderName" placeholder="e.g., Products, Blog Images, Portfolio">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="createFolder()">
<i class="bi bi-check-lg"></i> Create Folder
</button>
</div>
</div>
</div>
</div>
}

154
Views/AdminUsers/Create.cshtml Executable file
View File

@@ -0,0 +1,154 @@
@model SkyArtShop.Models.AdminUser
@{
ViewData["Title"] = "Create New User";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
var roles = ViewBag.Roles as List<string> ?? new List<string>();
}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="fas fa-user-plus"></i> Create New User</h2>
<a href="/admin/users" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Users
</a>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-body">
<form method="post" asp-action="Create">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Full Name *</label>
<input type="text" class="form-control" asp-for="Name" required />
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Email Address *</label>
<input type="email" class="form-control" asp-for="Email" required />
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Password *</label>
<input type="password" class="form-control" name="password" required minlength="6" />
<small class="text-muted">Minimum 6 characters</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Phone Number</label>
<input type="tel" class="form-control" asp-for="Phone" />
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Role *</label>
<select class="form-select" asp-for="Role" id="roleSelect" required>
@foreach (var role in roles)
{
<option value="@role">@role</option>
}
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Status</label>
<select class="form-select" asp-for="IsActive">
<option value="true">Active</option>
<option value="false">Inactive</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">Notes</label>
<textarea class="form-control" asp-for="Notes" rows="3" placeholder="Optional notes about this user..."></textarea>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="/admin/users" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Create User
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-info-circle"></i> Role Permissions</h5>
</div>
<div class="card-body">
<div id="rolePermissions">
<!-- Permissions will be displayed here based on selected role -->
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const rolePermissions = {
'MasterAdmin': [
'Manage Users',
'Manage Products',
'Manage Orders',
'Manage Content',
'Manage Settings',
'View Reports',
'Manage Finances',
'Manage Inventory',
'Manage Customers',
'Manage Blog & Portfolio',
'Full System Access'
],
'Admin': [
'Manage Products',
'Manage Orders',
'Manage Content',
'View Reports',
'Manage Inventory',
'Manage Customers',
'Manage Blog & Portfolio'
],
'Cashier': [
'View Products',
'Manage Orders',
'Process Payments',
'View Customers'
],
'Accountant': [
'View Products',
'View Orders',
'View Reports',
'Manage Finances',
'View Customers',
'Export Data'
]
};
function updateRolePermissions() {
const role = document.getElementById('roleSelect').value;
const permissions = rolePermissions[role] || [];
const container = document.getElementById('rolePermissions');
if (permissions.length > 0) {
let html = '<ul class="list-unstyled mb-0">';
permissions.forEach(perm => {
html += '<li class="mb-2"><i class="fas fa-check text-success"></i> ' + perm + '</li>';
});
html += '</ul>';
container.innerHTML = html;
}
}
document.getElementById('roleSelect').addEventListener('change', updateRolePermissions);
updateRolePermissions(); // Initialize on page load
</script>

171
Views/AdminUsers/Details.cshtml Executable file
View File

@@ -0,0 +1,171 @@
@model SkyArtShop.Models.AdminUser
@{
ViewData["Title"] = "View User";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="fas fa-user"></i> User Details</h2>
<div>
<a href="/admin/users/edit/@Model.Id" class="btn btn-warning">
<i class="fas fa-edit"></i> Edit User
</a>
<a href="/admin/users" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Users
</a>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-info-circle"></i> Basic Information</h5>
</div>
<div class="card-body">
<table class="table table-borderless">
<tbody>
<tr>
<th width="200">Full Name:</th>
<td><strong>@Model.Name</strong></td>
</tr>
<tr>
<th>Email:</th>
<td>@Model.Email</td>
</tr>
<tr>
<th>Phone:</th>
<td>@(string.IsNullOrEmpty(Model.Phone) ? "Not provided" : Model.Phone)</td>
</tr>
<tr>
<th>Role:</th>
<td>
@if (Model.Role == "MasterAdmin")
{
<span class="badge bg-danger">Master Admin</span>
}
else if (Model.Role == "Admin")
{
<span class="badge bg-primary">Admin</span>
}
else if (Model.Role == "Cashier")
{
<span class="badge bg-success">Cashier</span>
}
else if (Model.Role == "Accountant")
{
<span class="badge bg-info">Accountant</span>
}
</td>
</tr>
<tr>
<th>Status:</th>
<td>
@if (Model.IsActive)
{
<span class="badge bg-success"><i class="bi bi-check-circle-fill"></i> Active</span>
}
else
{
<span class="badge bg-danger"><i class="bi bi-x-circle-fill"></i> Inactive</span>
}
</td>
</tr>
<tr>
<th>Created:</th>
<td>@Model.CreatedAt.ToString("MMMM dd, yyyy HH:mm")</td>
</tr>
<tr>
<th>Created By:</th>
<td>@Model.CreatedBy</td>
</tr>
<tr>
<th>Last Login:</th>
<td>
@if (Model.LastLogin.HasValue)
{
@Model.LastLogin.Value.ToString("MMMM dd, yyyy HH:mm")
}
else
{
<span class="text-muted">Never logged in</span>
}
</td>
</tr>
</tbody>
</table>
@if (!string.IsNullOrEmpty(Model.Notes))
{
<div class="mt-3">
<h6>Notes:</h6>
<p class="text-muted">@Model.Notes</p>
</div>
}
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-key"></i> Permissions</h5>
</div>
<div class="card-body">
@if (Model.Permissions != null && Model.Permissions.Any())
{
<ul class="list-unstyled mb-0">
@foreach (var permission in Model.Permissions)
{
<li class="mb-2">
<i class="fas fa-check text-success"></i>
@{
var displayPerm = permission.Replace("_", " ");
displayPerm = char.ToUpper(displayPerm[0]) + displayPerm.Substring(1);
}
@displayPerm
</li>
}
</ul>
}
else
{
<p class="text-muted mb-0">No permissions assigned.</p>
}
</div>
</div>
<div class="card mt-3">
<div class="card-body">
<h6 class="mb-3"><i class="fas fa-cog"></i> Quick Actions</h6>
<div class="d-grid gap-2">
<a href="/admin/users/edit/@Model.Id" class="btn btn-sm btn-warning">
<i class="fas fa-edit"></i> Edit User
</a>
@if (Model.Role != "MasterAdmin")
{
<button class="btn btn-sm btn-danger" onclick="deleteUser('@Model.Id', '@Model.Name')">
<i class="fas fa-trash"></i> Delete User
</button>
}
</div>
</div>
</div>
</div>
</div>
</div>
<form method="post" id="deleteForm">
<input type="hidden" name="id" id="deleteUserId" />
</form>
<script>
function deleteUser(id, name) {
if (confirm('Are you sure you want to delete user: ' + name + '?\n\nThis action cannot be undone.')) {
var form = document.getElementById('deleteForm');
form.action = '/admin/users/delete/' + id;
form.submit();
}
}
</script>

137
Views/AdminUsers/Edit.cshtml Executable file
View File

@@ -0,0 +1,137 @@
@model SkyArtShop.Models.AdminUser
@{
ViewData["Title"] = "Edit User";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
var roles = ViewBag.Roles as List<string> ?? new List<string>();
}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="fas fa-user-edit"></i> Edit User</h2>
<a href="/admin/users" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Users
</a>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-body">
<form method="post" asp-action="Edit" asp-route-id="@Model.Id">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Full Name *</label>
<input type="text" class="form-control" asp-for="Name" required />
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Email Address *</label>
<input type="email" class="form-control" asp-for="Email" required />
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">New Password</label>
<input type="password" class="form-control" name="newPassword" minlength="6" />
<small class="text-muted">Leave blank to keep current password</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Phone Number</label>
<input type="tel" class="form-control" asp-for="Phone" />
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Role *</label>
@if (Model.Role == "MasterAdmin")
{
<select class="form-select" asp-for="Role" id="roleSelect" required disabled>
@foreach (var role in roles)
{
<option value="@role" selected="@(role == Model.Role)">@role</option>
}
</select>
<small class="text-muted">Master Admin role cannot be changed</small>
<input type="hidden" asp-for="Role" />
}
else
{
<select class="form-select" asp-for="Role" id="roleSelect" required>
@foreach (var role in roles)
{
<option value="@role" selected="@(role == Model.Role)">@role</option>
}
</select>
}
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Status</label>
<select class="form-select" asp-for="IsActive">
<option value="true">Active</option>
<option value="false">Inactive</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">Notes</label>
<textarea class="form-control" asp-for="Notes" rows="3"></textarea>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
Created: @Model.CreatedAt.ToString("MMMM dd, yyyy HH:mm") by @Model.CreatedBy
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="/admin/users" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Update User
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-info-circle"></i> Current Permissions</h5>
</div>
<div class="card-body">
@if (Model.Permissions != null && Model.Permissions.Any())
{
<ul class="list-unstyled mb-0">
@foreach (var permission in Model.Permissions)
{
<li class="mb-2">
<i class="fas fa-check text-success"></i>
@permission.Replace("_", " ").Replace("manage", "Manage").Replace("view", "View")
</li>
}
</ul>
}
else
{
<p class="text-muted mb-0">No specific permissions assigned.</p>
}
</div>
</div>
@if (Model.LastLogin.HasValue)
{
<div class="card mt-3">
<div class="card-body">
<h6><i class="fas fa-clock"></i> Last Login</h6>
<p class="mb-0">@Model.LastLogin.Value.ToString("MMMM dd, yyyy HH:mm")</p>
</div>
</div>
}
</div>
</div>
</div>

178
Views/AdminUsers/Index.cshtml Executable file
View File

@@ -0,0 +1,178 @@
@model List<SkyArtShop.Models.AdminUser>
@{
ViewData["Title"] = "User Management";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="fas fa-users"></i> User Management</h2>
<a href="/admin/users/create" class="btn btn-primary">
<i class="fas fa-plus"></i> Add New User
</a>
</div>
@if (TempData["Success"] != null)
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
@TempData["Success"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
@if (TempData["Error"] != null)
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@TempData["Error"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
<div class="card">
<div class="card-body">
@if (Model.Any())
{
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Phone</th>
<th>Status</th>
<th>Created</th>
<th>Last Login</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var user in Model)
{
<tr>
<td>
<strong>@user.Name</strong>
@if (user.Role == "MasterAdmin")
{
<span class="badge bg-danger ms-1">Master</span>
}
</td>
<td>@user.Email</td>
<td>
@if (user.Role == "MasterAdmin")
{
<span class="badge bg-danger">Master Admin</span>
}
else if (user.Role == "Admin")
{
<span class="badge bg-primary">Admin</span>
}
else if (user.Role == "Cashier")
{
<span class="badge bg-success">Cashier</span>
}
else if (user.Role == "Accountant")
{
<span class="badge bg-info">Accountant</span>
}
</td>
<td>@user.Phone</td>
<td>
@if (user.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-secondary">Inactive</span>
}
</td>
<td>@user.CreatedAt.ToString("MMM dd, yyyy")</td>
<td>
@if (user.LastLogin.HasValue)
{
@user.LastLogin.Value.ToString("MMM dd, yyyy HH:mm")
}
else
{
<span class="text-muted">Never</span>
}
</td>
<td>
<div class="btn-group" role="group">
<a href="/admin/users/view/@user.Id" class="btn btn-sm btn-info" title="View">
<i class="fas fa-eye"></i>
</a>
<a href="/admin/users/edit/@user.Id" class="btn btn-sm btn-warning" title="Edit">
<i class="fas fa-edit"></i>
</a>
@if (user.Role != "MasterAdmin")
{
<button type="button" class="btn btn-sm btn-danger" title="Delete"
onclick="deleteUser('@user.Id', '@user.Name')">
<i class="fas fa-trash"></i>
</button>
}
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="text-center py-5">
<i class="fas fa-users fa-3x text-muted mb-3"></i>
<p class="text-muted">No users found. Create your first user to get started.</p>
<a href="/admin/users/create" class="btn btn-primary">
<i class="fas fa-plus"></i> Add New User
</a>
</div>
}
</div>
</div>
<!-- Role Permissions Reference -->
<div class="card mt-4">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-info-circle"></i> Role Permissions</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<h6 class="text-danger"><i class="fas fa-crown"></i> Master Admin</h6>
<small class="text-muted">Full system access, can manage all users and settings</small>
</div>
<div class="col-md-3">
<h6 class="text-primary"><i class="fas fa-user-shield"></i> Admin</h6>
<small class="text-muted">Manage products, orders, content, and reports</small>
</div>
<div class="col-md-3">
<h6 class="text-success"><i class="fas fa-cash-register"></i> Cashier</h6>
<small class="text-muted">Process orders and payments, view products</small>
</div>
<div class="col-md-3">
<h6 class="text-info"><i class="fas fa-calculator"></i> Accountant</h6>
<small class="text-muted">View reports, manage finances, export data</small>
</div>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<form method="post" id="deleteForm">
<input type="hidden" name="id" id="deleteUserId" />
</form>
<script>
function deleteUser(id, name) {
if (confirm('Are you sure you want to delete user: ' + name + '?')) {
var form = document.getElementById('deleteForm');
form.action = '/admin/users/delete/' + id;
form.submit();
}
}
</script>

446
Views/Home/Index.cshtml Executable file
View File

@@ -0,0 +1,446 @@
<!-- Image Picker Modal -->
<div class="modal fade" id="imagePickerModal" tabindex="-1" aria-labelledby="imagePickerModalLabel" aria-hidden="true" data-bs-backdrop="true" style="z-index: 1060;">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="imagePickerModalLabel">
<i class="bi bi-images"></i> Select or Upload Images
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Tab Navigation -->
<ul class="nav nav-tabs mb-3" id="imagePickerTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="library-tab" data-bs-toggle="tab" data-bs-target="#library" type="button" role="tab">
<i class="bi bi-folder2-open"></i> Image Library
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload" type="button" role="tab">
<i class="bi bi-cloud-upload"></i> Upload New
</button>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content" id="imagePickerTabContent">
<!-- Image Library Tab -->
<div class="tab-pane fade show active" id="library" role="tabpanel">
<div class="mb-3">
<div class="btn-group w-100 mb-2" role="group">
<button type="button" class="btn btn-outline-primary" id="showProductImagesBtn" onclick="showProductImages()">Product Images (<span id="productImageCount">0</span>)</button>
<button type="button" class="btn btn-outline-secondary active" id="showAllImagesBtn" onclick="showAllImages()">All Images</button>
</div>
<input type="text" class="form-control" id="imageSearchInput" placeholder="Search images..." onkeyup="filterImages()">
</div>
<div id="imageLibraryLoading" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading images...</p>
</div>
<div id="imageLibraryContent" style="display: none;">
<div class="row g-2" id="imageLibraryGrid" style="max-height: 500px; overflow-y: auto;">
<!-- Images will be loaded here dynamically -->
</div>
<div id="noImagesMessage" class="text-center py-5" style="display: none;">
<i class="bi bi-images" style="font-size: 48px; color: #ccc;"></i>
<p class="text-muted mt-2">No images found in library</p>
</div>
</div>
</div>
<!-- Upload Tab -->
<div class="tab-pane fade" id="upload" role="tabpanel">
<div class="border rounded p-4 text-center" style="border-style: dashed !important;">
<i class="bi bi-cloud-arrow-up" style="font-size: 48px; color: #0d6efd;"></i>
<h5 class="mt-3">Upload Images</h5>
<p class="text-muted">Drag and drop files here or click to browse</p>
<input type="file" class="form-control" id="newImageUpload" accept="image/*" multiple style="display: none;">
<button type="button" class="btn btn-primary" onclick="document.getElementById('newImageUpload').click()">
<i class="bi bi-folder2-open"></i> Choose Files
</button>
<small class="d-block mt-2 text-muted">Supported formats: JPG, PNG, GIF, WEBP</small>
</div>
<div id="uploadProgress" class="mt-3" style="display: none;">
<div class="progress">
<div id="uploadProgressBar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
</div>
<small class="text-muted" id="uploadProgressText">Uploading...</small>
</div>
<div id="uploadedImagesPreview" class="row g-2 mt-3">
<!-- Uploaded images preview -->
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="selectImagesBtn" onclick="confirmImageSelection()">
<i class="bi bi-check-lg"></i> Select Images (<span id="selectedCount">0</span>)
</button>
</div>
</div>
</div>
</div>
<style>
/* Ensure nested modals work correctly */
.modal-backdrop.show:nth-of-type(2) {
z-index: 1055;
}
#imagePickerModal.show {
z-index: 1060 !important;
}
.modal-backdrop.show {
z-index: 1050;
}
</style>
<script>
let selectedImages = [];
let imagePickerCallback = null;
let imagePickerMode = 'multiple'; // 'single' or 'multiple'
let allLibraryImages = [];
let currentViewMode = 'all'; // 'product' or 'all'
// Open Image Picker Modal
function openImagePicker(callback, mode = 'multiple') {
imagePickerCallback = callback;
imagePickerMode = mode;
selectedImages = [];
updateSelectedCount();
// Update product image count
const productImages = window.currentProductImages || [];
document.getElementById('productImageCount').textContent = productImages.length;
// Show modal - use getOrCreateInstance to handle existing instances
const modalElement = document.getElementById('imagePickerModal');
if (!modalElement) {
console.error('Image Picker Modal element not found!');
alert('Error: Image picker not available. Please refresh the page.');
return;
}
const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
modal.show();
// Start with product images if available, otherwise all images
if (productImages.length > 0) {
currentViewMode = 'product';
showProductImages();
} else {
currentViewMode = 'all';
loadImageLibrary();
}
}
// Load Image Library
async function loadImageLibrary() {
const loading = document.getElementById('imageLibraryLoading');
const content = document.getElementById('imageLibraryContent');
const grid = document.getElementById('imageLibraryGrid');
const noImagesMsg = document.getElementById('noImagesMessage');
loading.style.display = 'block';
content.style.display = 'none';
try {
const response = await fetch('/admin/upload/list');
const images = await response.json();
allLibraryImages = images;
loading.style.display = 'none';
content.style.display = 'block';
if (images.length === 0) {
grid.style.display = 'none';
noImagesMsg.style.display = 'block';
} else {
grid.style.display = 'flex';
noImagesMsg.style.display = 'none';
renderImageLibrary(images);
}
} catch (error) {
console.error('Error loading images:', error);
loading.innerHTML = '<div class="alert alert-danger">Error loading images</div>';
}
}
// Render Image Library
function renderImageLibrary(images) {
const grid = document.getElementById('imageLibraryGrid');
grid.innerHTML = '';
images.forEach(imageUrl => {
const col = document.createElement('div');
col.className = 'col-6 col-md-3 col-lg-2';
const itemDiv = document.createElement('div');
itemDiv.className = 'image-library-item position-relative';
itemDiv.setAttribute('data-image-url', imageUrl);
itemDiv.style.cssText = 'cursor: pointer; border: 3px solid transparent; border-radius: 8px; overflow: hidden; transition: all 0.2s;';
itemDiv.innerHTML = `
<img src="${imageUrl}" class="img-fluid" style="width: 100%; height: 120px; object-fit: cover; display: block;">
<div class="image-overlay position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"
style="background: rgba(0,0,0,0.5); opacity: 0; transition: opacity 0.2s;">
<i class="bi bi-check-circle-fill text-white" style="font-size: 2rem;"></i>
</div>
`;
// Use event listener instead of inline onclick to avoid URL escaping issues
itemDiv.addEventListener('click', function() {
toggleImageSelection(imageUrl, this);
});
col.appendChild(itemDiv);
grid.appendChild(col);
});
}
// Show Product Images
function showProductImages() {
currentViewMode = 'product';
const productImages = window.currentProductImages || [];
// Update button states
document.getElementById('showProductImagesBtn').classList.add('active');
document.getElementById('showProductImagesBtn').classList.remove('btn-outline-primary');
document.getElementById('showProductImagesBtn').classList.add('btn-primary');
document.getElementById('showAllImagesBtn').classList.remove('active', 'btn-primary');
document.getElementById('showAllImagesBtn').classList.add('btn-outline-secondary');
const loading = document.getElementById('imageLibraryLoading');
const content = document.getElementById('imageLibraryContent');
const grid = document.getElementById('imageLibraryGrid');
const noImagesMsg = document.getElementById('noImagesMessage');
loading.style.display = 'none';
content.style.display = 'block';
if (productImages.length === 0) {
grid.style.display = 'none';
noImagesMsg.style.display = 'block';
noImagesMsg.innerHTML = '<i class=\"bi bi-images\" style=\"font-size: 48px; color: #ccc;\"></i><p class=\"text-muted mt-2\">No images added to this product yet. Switch to \"All Images\" to browse the full library.</p>';
} else {
grid.style.display = 'flex';
noImagesMsg.style.display = 'none';
renderImageLibrary(productImages);
}
}
// Show All Images
function showAllImages() {
currentViewMode = 'all';
// Update button states
document.getElementById('showAllImagesBtn').classList.add('active');
document.getElementById('showAllImagesBtn').classList.remove('btn-outline-secondary');
document.getElementById('showAllImagesBtn').classList.add('btn-primary');
document.getElementById('showProductImagesBtn').classList.remove('active', 'btn-primary');
document.getElementById('showProductImagesBtn').classList.add('btn-outline-primary');
loadImageLibrary();
}
// Filter Images
function filterImages() {
const searchTerm = document.getElementById('imageSearchInput').value.toLowerCase();
if (currentViewMode === 'product') {
const productImages = window.currentProductImages || [];
const filteredImages = productImages.filter(url => url.toLowerCase().includes(searchTerm));
renderImageLibrary(filteredImages);
} else {
const filteredImages = allLibraryImages.filter(url => url.toLowerCase().includes(searchTerm));
// Toggle Image Selection
function toggleImageSelection(imageUrl, element) {
if (imagePickerMode === 'single') {
// Single selection mode
selectedImages = [imageUrl];
// Remove selection from all
document.querySelectorAll('.image-library-item').forEach(item => {
item.style.borderColor = 'transparent';
item.querySelector('.image-overlay').style.opacity = '0';
});
// Add selection to clicked
element.style.borderColor = '#0d6efd';
element.querySelector('.image-overlay').style.opacity = '1';
} else {
// Multiple selection mode
const index = selectedImages.indexOf(imageUrl);
if (index > -1) {
selectedImages.splice(index, 1);
element.style.borderColor = 'transparent';
element.querySelector('.image-overlay').style.opacity = '0';
} else {
selectedImages.push(imageUrl);
element.style.borderColor = '#0d6efd';
element.querySelector('.image-overlay').style.opacity = '1';
}
}
updateSelectedCount();
}
// Update Selected Count
function updateSelectedCount() {
document.getElementById('selectedCount').textContent = selectedImages.length;
document.getElementById('selectImagesBtn').disabled = selectedImages.length === 0;
}
// Confirm Image Selection
function confirmImageSelection() {
if (selectedImages.length === 0) {
alert('Please select at least one image.');
return;
}
if (!imagePickerCallback) {
console.error('No callback function defined!');
alert('Error: Callback function not found. Please close and try again.');
return;
}
try {
imagePickerCallback(selectedImages);
const modalElement = document.getElementById('imagePickerModal');
const modalInstance = bootstrap.Modal.getInstance(modalElement);
if (modalInstance) {
modalInstance.hide();
}
} catch (error) {
console.error('Error in callback:', error);
alert('Error processing selection: ' + error.message);
}
}
// Handle New Image Upload
document.addEventListener('DOMContentLoaded', function() {
const uploadInput = document.getElementById('newImageUpload');
if (uploadInput) {
uploadInput.addEventListener('change', handleNewImageUpload);
}
// Drag and drop support
const uploadArea = document.querySelector('#upload .border');
if (uploadArea) {
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.style.backgroundColor = '#e7f3ff';
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.style.backgroundColor = '';
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.style.backgroundColor = '';
const files = e.dataTransfer.files;
handleNewImageUpload({ target: { files } });
});
}
});
// Handle New Image Upload
async function handleNewImageUpload(event) {
const files = event.target.files;
if (files.length === 0) return;
const progressDiv = document.getElementById('uploadProgress');
const progressBar = document.getElementById('uploadProgressBar');
const progressText = document.getElementById('uploadProgressText');
const previewDiv = document.getElementById('uploadedImagesPreview');
progressDiv.style.display = 'block';
previewDiv.innerHTML = '';
const uploadedUrls = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const formData = new FormData();
formData.append('file', file);
progressText.textContent = `Uploading ${i + 1} of ${files.length}...`;
progressBar.style.width = ((i / files.length) * 100) + '%';
try {
const response = await fetch('/admin/upload/image', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
uploadedUrls.push(result.url);
// Add to preview
const col = document.createElement('div');
col.className = 'col-4';
col.innerHTML = `
<div class="position-relative">
<img src="${result.url}" class="img-fluid rounded" style="width: 100%; height: 100px; object-fit: cover;">
<span class="badge bg-success position-absolute top-0 end-0 m-1">
<i class="bi bi-check-lg"></i>
</span>
</div>
`;
previewDiv.appendChild(col);
}
} catch (error) {
console.error('Upload error:', error);
}
}
progressBar.style.width = '100%';
progressText.textContent = `Successfully uploaded ${uploadedUrls.length} images`;
// Auto-select uploaded images
selectedImages = uploadedUrls;
updateSelectedCount();
// Reload library
setTimeout(() => {
loadImageLibrary();
// Switch to library tab
document.getElementById('library-tab').click();
}, 1000);
// Reset input
event.target.value = '';
}
</script>
<style>
.image-library-item:hover {
transform: scale(1.05);
}
.image-library-item:hover .image-overlay {
opacity: 0.3 !important;
}
#imageLibraryGrid {
scrollbar-width: thin;
scrollbar-color: #0d6efd #f8f9fa;
}
#imageLibraryGrid::-webkit-scrollbar {
width: 8px;
}
#imageLibraryGrid::-webkit-scrollbar-track {
background: #f8f9fa;
}
#imageLibraryGrid::-webkit-scrollbar-thumb {
background: #0d6efd;
border-radius: 4px;
}
</style>

114
Views/Page/Index.cshtml Executable file
View File

@@ -0,0 +1,114 @@
@model SkyArtShop.Models.Page
@{
ViewData["Title"] = Model?.Title ?? "About";
}
<!-- About Hero Section -->
<section class="about-hero">
<div class="container">
<h1>@(Model?.Title ?? "About Sky Art Shop")</h1>
@if (!string.IsNullOrEmpty(Model?.Subtitle))
{
<p class="hero-subtitle">@Model.Subtitle</p>
}
</div>
</section>
<!-- About Content Section -->
<section class="about-content">
<div class="container">
<div class="about-layout">
<div class="about-main-content">
@if (!string.IsNullOrEmpty(Model?.Content))
{
<div class="content-wrapper">
@Html.Raw(Model.Content)
</div>
}
else
{
<div class="about-text">
<h2>Our Story</h2>
<p>
Sky Art Shop specializes in scrapbooking, journaling, cardmaking,
and collaging stationery. We are passionate about helping people
express their creativity and preserve their memories.
</p>
<p>
Our mission is to promote mental health and wellness through
creative art activities. We believe that crafting is more than
just a hobby—it's a therapeutic journey that brings joy,
mindfulness, and self-expression.
</p>
<h2>What We Offer</h2>
<p>Our carefully curated collection includes:</p>
<ul>
<li>Washi tape in various designs and patterns</li>
<li>Unique stickers for journaling and scrapbooking</li>
<li>High-quality journals and notebooks</li>
<li>Card making supplies and kits</li>
<li>Scrapbooking materials and embellishments</li>
<li>Collage papers and ephemera</li>
</ul>
</div>
}
</div>
@if (!string.IsNullOrEmpty(Model?.AboutImage1) || !string.IsNullOrEmpty(Model?.AboutImage2))
{
<div class="about-sidebar">
<div class="sidebar-images">
@if (!string.IsNullOrEmpty(Model.AboutImage1))
{
<div class="sidebar-image-item">
<img src="@Model.AboutImage1" alt="About image 1" />
</div>
}
@if (!string.IsNullOrEmpty(Model.AboutImage2))
{
<div class="sidebar-image-item">
<img src="@Model.AboutImage2" alt="About image 2" />
</div>
}
</div>
</div>
}
</div>
</div>
</section>
@if (Model?.TeamMembers != null && Model.TeamMembers.Any())
{
<!-- Team Section -->
<section class="team-section">
<div class="container">
<div class="section-header text-center mb-5">
<h2>Meet Our Team</h2>
<p class="lead">The creative minds behind Sky Art Shop</p>
</div>
<div class="team-grid">
@foreach (var member in Model.TeamMembers)
{
<div class="team-member-card">
<div class="team-member-info">
<h3 class="member-name">@member.Name</h3>
@if (!string.IsNullOrEmpty(member.Role))
{
<p class="member-role">@member.Role</p>
}
@if (!string.IsNullOrEmpty(member.Bio))
{
<p class="member-bio">@member.Bio</p>
}
</div>
<div class="team-member-photo">
<img src="@(!string.IsNullOrEmpty(member.PhotoUrl) ? member.PhotoUrl : "/assets/images/placeholder.jpg")"
alt="@member.Name" />
</div>
</div>
}
</div>
</div>
</section>
}

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>