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:
45
Views/Admin/ChangePassword.cshtml
Executable file
45
Views/Admin/ChangePassword.cshtml
Executable 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
189
Views/Admin/Dashboard.cshtml
Executable 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
430
Views/Admin/Login.cshtml
Executable 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
430
Views/Admin/Settings.cshtml
Executable 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
75
Views/Admin/Test.cshtml
Executable 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
153
Views/AdminBlog/Create.cshtml
Executable 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
153
Views/AdminBlog/Edit.cshtml
Executable 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
172
Views/AdminHomepage/Create.cshtml
Executable 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
233
Views/AdminHomepage/Edit.cshtml
Executable 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
252
Views/AdminHomepage/Index.cshtml
Executable 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
81
Views/AdminMenu/Create.cshtml
Executable 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
136
Views/AdminMenu/Edit.cshtml
Executable 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
86
Views/AdminMenu/Index.cshtml
Executable 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
133
Views/AdminPages/Create.cshtml
Executable 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
489
Views/AdminPages/Edit.cshtml
Executable 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>
|
||||
}
|
||||
120
Views/AdminPortfolio/CreateCategory.cshtml
Executable file
120
Views/AdminPortfolio/CreateCategory.cshtml
Executable 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>
|
||||
}
|
||||
120
Views/AdminPortfolio/EditCategory.cshtml
Executable file
120
Views/AdminPortfolio/EditCategory.cshtml
Executable 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
582
Views/AdminProducts/Form.cshtml
Executable 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
109
Views/AdminProducts/Index.cshtml
Executable 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
612
Views/AdminUpload/Index.cshtml
Executable 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
154
Views/AdminUsers/Create.cshtml
Executable 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
171
Views/AdminUsers/Details.cshtml
Executable 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
137
Views/AdminUsers/Edit.cshtml
Executable 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
178
Views/AdminUsers/Index.cshtml
Executable 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
446
Views/Home/Index.cshtml
Executable 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
114
Views/Page/Index.cshtml
Executable 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
715
Views/Shop/Details.cshtml
Executable file
@@ -0,0 +1,715 @@
|
||||
@model SkyArtShop.Models.Product
|
||||
@{
|
||||
ViewData["Title"] = Model.Name;
|
||||
}
|
||||
|
||||
<section class="product-detail-modern">
|
||||
<div class="container">
|
||||
<div class="product-split">
|
||||
<!-- LEFT: Gallery -->
|
||||
<div class="image-pane">
|
||||
<div class="gallery">
|
||||
<div class="gallery-sidebar">
|
||||
<div class="gallery-thumbs">
|
||||
@if (Model.Images != null && Model.Images.Count > 0)
|
||||
{
|
||||
@for (int i = 0; i < Model.Images.Count; i++)
|
||||
{
|
||||
var image = Model.Images[i];
|
||||
var isFirst = i == 0;
|
||||
<div class="thumb @(isFirst ? "active" : "")" data-src="@image" onclick="setImage(this)">
|
||||
<img src="@image" alt="@Model.Name">
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.ImageUrl))
|
||||
{
|
||||
<div class="thumb active" data-src="@Model.ImageUrl" onclick="setImage(this)">
|
||||
<img src="@Model.ImageUrl" alt="@Model.Name">
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="thumb active" data-src="/assets/images/placeholder.jpg" onclick="setImage(this)">
|
||||
<img src="/assets/images/placeholder.jpg" alt="@Model.Name">
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="zoom-hint"><i class="bi bi-zoom-in"></i> Click to view full size</div>
|
||||
</div>
|
||||
<div class="gallery-main" onclick="openLightbox()">
|
||||
<button class="nav prev" type="button" onclick="event.stopPropagation(); slideImage(-1)"><i class="bi bi-chevron-left"></i></button>
|
||||
@{
|
||||
var mainImageSrc = Model.Images != null && Model.Images.Count > 0
|
||||
? Model.Images[0]
|
||||
: (!string.IsNullOrEmpty(Model.ImageUrl) ? Model.ImageUrl : "/assets/images/placeholder.jpg");
|
||||
}
|
||||
<img id="galleryImage" src="@mainImageSrc" alt="@Model.Name">
|
||||
<button class="nav next" type="button" onclick="event.stopPropagation(); slideImage(1)"><i class="bi bi-chevron-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: Details -->
|
||||
<div class="info-pane">
|
||||
<div class="details">
|
||||
<h1 class="title">@Model.Name</h1>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.ShortDescription))
|
||||
{
|
||||
<p class="short-description">@Model.ShortDescription</p>
|
||||
}
|
||||
|
||||
<div class="meta">
|
||||
<div class="meta-left">
|
||||
@if (!string.IsNullOrEmpty(Model.SKU))
|
||||
{
|
||||
<span class="sku">SKU: @Model.SKU</span>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.Category))
|
||||
{
|
||||
<span class="sku">SKU: @Model.Category.ToUpper().Replace(" ","")@Model.Id?.Substring(Model.Id.Length - 4)</span>
|
||||
}
|
||||
@{
|
||||
var rating = Model.AverageRating > 0 ? Model.AverageRating : 5.0;
|
||||
var fullStars = (int)Math.Floor(rating);
|
||||
var hasHalfStar = (rating - fullStars) >= 0.5;
|
||||
var emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
|
||||
}
|
||||
<div class="stars">
|
||||
@for (int i = 0; i < fullStars; i++)
|
||||
{
|
||||
<i class="bi bi-star-fill"></i>
|
||||
}
|
||||
@if (hasHalfStar)
|
||||
{
|
||||
<i class="bi bi-star-half"></i>
|
||||
}
|
||||
@for (int i = 0; i < emptyStars; i++)
|
||||
{
|
||||
<i class="bi bi-star"></i>
|
||||
}
|
||||
<span class="rating-text">(@Model.TotalReviews review@(Model.TotalReviews != 1 ? "s" : ""))</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.UnitsSold > 0)
|
||||
{
|
||||
<span class="units-sold">@Model.UnitsSold sold</span>
|
||||
}
|
||||
</div>
|
||||
<!-- Price first -->
|
||||
<div class="price-row">
|
||||
<span class="label">Price:</span>
|
||||
<span class="price">$@Model.Price.ToString("F2")</span>
|
||||
</div>
|
||||
<!-- Stock info under price -->
|
||||
<div class="stock-row">
|
||||
@if (Model.StockQuantity > 0)
|
||||
{
|
||||
<div class="stock ok"><i class="bi bi-check-circle-fill"></i> In stock (@Model.StockQuantity+
|
||||
units), ready to be shipped</div>
|
||||
<div class="stock-bar green"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
<!-- Actions below quantity and color -->
|
||||
<div class="actions">
|
||||
@if (Model.StockQuantity > 0)
|
||||
{
|
||||
<button class="cta" onclick="addToCartFromDetail()"><i class="bi bi-cart-plus"></i> Add to Cart</button>
|
||||
<button class="cta alt" onclick="addToWishlistFromDetail()"><i class="bi bi-heart"></i> Add to Wishlist</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="cta" disabled>Out of Stock</button>
|
||||
}
|
||||
</div>
|
||||
<div class="stock bad"><i class="bi bi-x-circle-fill"></i> Out of stock</div>
|
||||
<div class="stock-bar red"></div>
|
||||
}
|
||||
</div>
|
||||
<!-- Quantity next -->
|
||||
<div class="qty-row">
|
||||
<div class="qty-header">
|
||||
<span class="label">Quantity:</span>
|
||||
@if (Model.StockQuantity > 0)
|
||||
{
|
||||
<span class="stock-count">(@Model.StockQuantity available)</span>
|
||||
}
|
||||
</div>
|
||||
<div class="qty">
|
||||
<button type="button" class="qty-btn" onclick="decreaseQuantity()" @(Model.StockQuantity == 0 ?
|
||||
"disabled" : "")><i class="bi bi-dash"></i></button>
|
||||
<input id="quantity" type="number" value="1" min="1" max="@Model.StockQuantity" readonly>
|
||||
<button type="button" class="qty-btn" onclick="increaseQuantity()" @(Model.StockQuantity == 0 ?
|
||||
"disabled" : "")><i class="bi bi-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Actions below quantity -->
|
||||
<div class="actions">
|
||||
@if (Model.StockQuantity > 0)
|
||||
{
|
||||
<button class="cta" onclick="addToCartFromDetail()"><i class="bi bi-cart-plus"></i> Add to Cart</button>
|
||||
<button class="cta alt" onclick="addToWishlistFromDetail()"><i class="bi bi-heart"></i> Add to Wishlist</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="cta" disabled>Out of Stock</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Color Variant Selector -->
|
||||
@{
|
||||
var hasVariants = Model.Variants != null && Model.Variants.Any(v => v.IsAvailable && v.Images != null && v.Images.Any());
|
||||
var hasLegacyColors = (Model.Colors != null && Model.Colors.Any()) || !string.IsNullOrEmpty(Model.Color);
|
||||
|
||||
if (hasVariants && Model.Variants != null)
|
||||
{
|
||||
// New variant system - filter only available variants with images
|
||||
var availableVariants = Model.Variants.Where(v => v.IsAvailable && v.Images != null && v.Images.Any()).ToList();
|
||||
|
||||
<div class="color-section variant-section">
|
||||
<div class="color-row">
|
||||
<span class="label">Select Color:</span>
|
||||
<span class="value" id="selectedVariantName">Choose a color</span>
|
||||
</div>
|
||||
<div class="variant-swatches" id="variantSwatches">
|
||||
@foreach (var variant in availableVariants)
|
||||
{
|
||||
var variantJson = System.Text.Json.JsonSerializer.Serialize(new {
|
||||
colorName = variant.ColorName,
|
||||
colorHex = variant.ColorHex,
|
||||
images = variant.Images,
|
||||
stock = variant.StockQuantity,
|
||||
priceAdjust = variant.PriceAdjustment,
|
||||
sku = variant.SKU
|
||||
});
|
||||
|
||||
<div class="variant-swatch"
|
||||
data-variant='@Html.Raw(variantJson)'
|
||||
onclick="selectVariant(this)"
|
||||
title="@variant.ColorName (@variant.StockQuantity in stock)">
|
||||
<span class="variant-dot" style="background-color: @variant.ColorHex; box-shadow: 0 0 0 2px white, 0 0 0 3px #ddd;"></span>
|
||||
<span class="variant-name">@variant.ColorName</span>
|
||||
@if (variant.StockQuantity <= 5 && variant.StockQuantity > 0)
|
||||
{
|
||||
<span class="variant-badge">Only @variant.StockQuantity left</span>
|
||||
}
|
||||
@if (variant.StockQuantity == 0)
|
||||
{
|
||||
<span class="variant-badge out-of-stock">Out of Stock</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<input type="hidden" id="selectedVariantData" value="">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Store all variant data for easy access
|
||||
window.productVariants = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(availableVariants));
|
||||
|
||||
// Don't auto-select on page load - show all images initially
|
||||
// User can click a color to filter/highlight
|
||||
</script>
|
||||
}
|
||||
@* Legacy color system removed - all products should use Variants *@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.Description))
|
||||
{
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="description-container">
|
||||
<div class="description-tab" onclick="toggleDescription()">
|
||||
<span>Description</span>
|
||||
<i class="bi bi-chevron-down" id="descChevron"></i>
|
||||
</div>
|
||||
<div class="description-box" id="descriptionBox">
|
||||
<div class="description-content" id="descriptionContent">
|
||||
@Html.Raw(Model.Description)
|
||||
</div>
|
||||
<button class="see-more-btn" id="seeMoreBtn" onclick="expandDescription()" style="display: none;">
|
||||
<i class="bi bi-chevron-down"></i> See More
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Product Description Tabs -->
|
||||
|
||||
|
||||
<!-- Related Products Section -->
|
||||
@if (ViewBag.RelatedProducts != null && ViewBag.RelatedProducts.Count > 0)
|
||||
{
|
||||
<div class="row mt-5">
|
||||
<div class="col-12">
|
||||
<h3 class="section-title mb-3">You May Also Like</h3>
|
||||
<p class="text-muted mb-4">Based on what customers are viewing</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="products-grid mb-4">
|
||||
@foreach (var relatedProduct in ViewBag.RelatedProducts)
|
||||
{
|
||||
<div class="product-card">
|
||||
<a href="/shop/product/@relatedProduct.Id" class="product-link">
|
||||
<div class="product-image">
|
||||
<img src="@(string.IsNullOrEmpty(relatedProduct.ImageUrl) ? "/assets/images/placeholder.jpg" : relatedProduct.ImageUrl)"
|
||||
alt="@relatedProduct.Name" loading="lazy">
|
||||
</div>
|
||||
<h3>@relatedProduct.Name</h3>
|
||||
@if (!string.IsNullOrEmpty(relatedProduct.Color))
|
||||
{
|
||||
<span class="product-color-badge">@relatedProduct.Color</span>
|
||||
}
|
||||
<div class="product-description">@Html.Raw(relatedProduct.ShortDescription ?? relatedProduct.Description)</div>
|
||||
<p class="price">$@relatedProduct.Price.ToString("F2")</p>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<a href="/shop?category=@Model.Category" class="btn btn-outline-primary">
|
||||
Browse More @Model.Category Products
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row mt-5">
|
||||
<div class="col-12 text-center">
|
||||
<h3 class="section-title mb-3">Explore Our Collection</h3>
|
||||
<a href="/shop?category=@Model.Category" class="btn btn-outline-primary">
|
||||
Browse @Model.Category
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// Simple slider/gallery with fade transition + hover zoom
|
||||
const images = [
|
||||
@if (Model.Images != null && Model.Images.Count > 0)
|
||||
{
|
||||
@for (int i = 0; i < Model.Images.Count; i++)
|
||||
{
|
||||
@: '@Model.Images[i]'@(i < Model.Images.Count - 1 ? "," : "")
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.ImageUrl))
|
||||
{
|
||||
@: '@Model.ImageUrl'
|
||||
}
|
||||
else
|
||||
{
|
||||
@: '/assets/images/placeholder.jpg'
|
||||
}
|
||||
];
|
||||
let currentIndex = 0;
|
||||
let animating = false;
|
||||
|
||||
function changeImage(nextSrc, direction = 0) {
|
||||
const img = document.getElementById('galleryImage');
|
||||
if (animating) return;
|
||||
animating = true;
|
||||
// small directional nudge for slide feel
|
||||
const shift = direction === 0 ? 0 : (direction > 0 ? 12 : -12);
|
||||
img.style.transform = `translateX(${shift}px) scale(1)`;
|
||||
// start fade-out
|
||||
img.classList.add('fade-out');
|
||||
const onTransitionEnd = () => {
|
||||
img.removeEventListener('transitionend', onTransitionEnd);
|
||||
img.onload = () => {
|
||||
// fade back in once new image is loaded
|
||||
requestAnimationFrame(() => {
|
||||
img.classList.remove('fade-out');
|
||||
img.style.transform = 'scale(1)';
|
||||
animating = false;
|
||||
});
|
||||
};
|
||||
img.src = nextSrc;
|
||||
};
|
||||
// If the browser doesn't fire transitionend (short durations), fallback
|
||||
img.addEventListener('transitionend', onTransitionEnd);
|
||||
// Fallback timeout (safety)
|
||||
setTimeout(() => {
|
||||
if (img.classList.contains('fade-out')) {
|
||||
onTransitionEnd();
|
||||
}
|
||||
}, 220);
|
||||
}
|
||||
|
||||
function setImage(el) {
|
||||
const src = el.getAttribute('data-src');
|
||||
changeImage(src, 0);
|
||||
document.querySelectorAll('.gallery-thumbs .thumb').forEach(t => t.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
currentIndex = images.indexOf(src);
|
||||
|
||||
// Clear variant filtering when manually clicking an image
|
||||
const gallery = document.querySelector('.gallery');
|
||||
if (gallery) {
|
||||
gallery.classList.remove('variant-filtering');
|
||||
}
|
||||
|
||||
// Clear variant selection visual
|
||||
document.querySelectorAll('.variant-swatch').forEach(s => s.classList.remove('selected'));
|
||||
document.querySelectorAll('.variant-dot').forEach(d => {
|
||||
d.style.boxShadow = '0 0 0 2px white, 0 0 0 3px #ddd';
|
||||
});
|
||||
|
||||
// Clear variant match classes
|
||||
document.querySelectorAll('.gallery-thumbs .thumb').forEach(t => t.classList.remove('variant-match'));
|
||||
|
||||
// Clear selected variant data
|
||||
const variantNameEl = document.getElementById('selectedVariantName');
|
||||
if (variantNameEl) {
|
||||
variantNameEl.textContent = 'Choose a color';
|
||||
}
|
||||
document.getElementById('selectedVariantData').value = '';
|
||||
}
|
||||
|
||||
function slideImage(direction) {
|
||||
currentIndex = (currentIndex + direction + images.length) % images.length;
|
||||
const nextSrc = images[currentIndex];
|
||||
changeImage(nextSrc, direction);
|
||||
|
||||
// Clear variant filtering when using arrow navigation
|
||||
const gallery = document.querySelector('.gallery');
|
||||
if (gallery) {
|
||||
gallery.classList.remove('variant-filtering');
|
||||
}
|
||||
|
||||
// Clear variant selection visual
|
||||
document.querySelectorAll('.variant-swatch').forEach(s => s.classList.remove('selected'));
|
||||
document.querySelectorAll('.variant-dot').forEach(d => {
|
||||
d.style.boxShadow = '0 0 0 2px white, 0 0 0 3px #ddd';
|
||||
});
|
||||
|
||||
// Clear variant match classes
|
||||
document.querySelectorAll('.gallery-thumbs .thumb').forEach(t => t.classList.remove('variant-match'));
|
||||
|
||||
// Clear selected variant data
|
||||
const variantNameEl = document.getElementById('selectedVariantName');
|
||||
if (variantNameEl) {
|
||||
variantNameEl.textContent = 'Choose a color';
|
||||
}
|
||||
document.getElementById('selectedVariantData').value = '';
|
||||
|
||||
// update active thumb
|
||||
document.querySelectorAll('.gallery-thumbs .thumb').forEach(t => {
|
||||
if (t.getAttribute('data-src') === nextSrc) t.classList.add('active'); else t.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
function increaseQuantity() {
|
||||
const input = document.getElementById('quantity');
|
||||
const max = parseInt(input.max);
|
||||
const current = parseInt(input.value);
|
||||
if (current < max) {
|
||||
input.value = current + 1;
|
||||
}
|
||||
}
|
||||
|
||||
function decreaseQuantity() {
|
||||
const input = document.getElementById('quantity');
|
||||
const current = parseInt(input.value);
|
||||
if (current > 1) {
|
||||
input.value = current - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function addToCartFromDetail() {
|
||||
const quantity = parseInt(document.getElementById('quantity').value);
|
||||
const productId = '@Model.Id';
|
||||
let productName = '@Model.Name';
|
||||
let productPrice = @Model.Price;
|
||||
let imageUrl = '@(Model.Images != null && Model.Images.Count > 0 ? Model.Images[0] : "/assets/images/placeholder.jpg")';
|
||||
|
||||
// Check if a variant is selected
|
||||
const selectedVariantEl = document.getElementById('selectedVariantData');
|
||||
if (selectedVariantEl && selectedVariantEl.value) {
|
||||
try {
|
||||
const variantData = JSON.parse(selectedVariantEl.value);
|
||||
|
||||
// Append color to product name
|
||||
productName += ` (${variantData.colorName})`;
|
||||
|
||||
// Apply price adjustment if any
|
||||
if (variantData.priceAdjust) {
|
||||
productPrice += variantData.priceAdjust;
|
||||
}
|
||||
|
||||
// Use variant's first image if available
|
||||
if (variantData.images && variantData.images.length > 0) {
|
||||
imageUrl = variantData.images[0];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing variant data:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Call the cart function multiple times for quantity
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
addToCart(productId, productName, productPrice, imageUrl);
|
||||
}
|
||||
|
||||
// Animate cart icon
|
||||
const cartBtn = document.getElementById('cartBtn');
|
||||
const cartBadge = cartBtn ? cartBtn.querySelector('.badge') : null;
|
||||
|
||||
if (cartBtn) {
|
||||
cartBtn.style.animation = 'cartBounce 0.6s ease';
|
||||
setTimeout(() => {
|
||||
cartBtn.style.animation = '';
|
||||
}, 600);
|
||||
}
|
||||
|
||||
if (cartBadge) {
|
||||
cartBadge.style.animation = 'badgePulse 0.6s ease';
|
||||
setTimeout(() => {
|
||||
cartBadge.style.animation = '';
|
||||
}, 600);
|
||||
}
|
||||
|
||||
// Show success toast
|
||||
showSuccessToast(`Added ${quantity} x ${productName} to cart!`);
|
||||
}
|
||||
|
||||
function addToWishlistFromDetail() {
|
||||
const productId = '@Model.Id';
|
||||
let productName = '@Model.Name';
|
||||
let productPrice = @Model.Price;
|
||||
let imageUrl = '@(Model.Images != null && Model.Images.Count > 0 ? Model.Images[0] : "/assets/images/placeholder.jpg")';
|
||||
|
||||
// Check if a variant is selected
|
||||
const selectedVariantEl = document.getElementById('selectedVariantData');
|
||||
if (selectedVariantEl && selectedVariantEl.value) {
|
||||
try {
|
||||
const variantData = JSON.parse(selectedVariantEl.value);
|
||||
|
||||
// Append color to product name
|
||||
productName += ` (${variantData.colorName})`;
|
||||
|
||||
// Apply price adjustment if any
|
||||
if (variantData.priceAdjust) {
|
||||
productPrice += variantData.priceAdjust;
|
||||
}
|
||||
|
||||
// Use variant's first image if available
|
||||
if (variantData.images && variantData.images.length > 0) {
|
||||
imageUrl = variantData.images[0];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing variant data:', e);
|
||||
}
|
||||
}
|
||||
|
||||
addToWishlist(productId, productName, productPrice, imageUrl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Lightbox Viewer
|
||||
function ensureLightbox() {
|
||||
let lb = document.getElementById('lightbox');
|
||||
if (lb) return lb;
|
||||
lb = document.createElement('div');
|
||||
lb.id = 'lightbox';
|
||||
lb.className = 'lightbox';
|
||||
lb.innerHTML = `
|
||||
<div class="lightbox-content">
|
||||
<button class="lb-nav lb-prev" type="button" aria-label="Previous" onclick="lbPrev(event)"><i class="bi bi-chevron-left"></i></button>
|
||||
<img id="lbImage" alt="@Model.Name" />
|
||||
<button class="lb-nav lb-next" type="button" aria-label="Next" onclick="lbNext(event)"><i class="bi bi-chevron-right"></i></button>
|
||||
<button class="lb-close" type="button" aria-label="Close" onclick="closeLightbox(event)"><i class="bi bi-x-lg"></i></button>
|
||||
</div>`;
|
||||
document.body.appendChild(lb);
|
||||
lb.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'lightbox') closeLightbox(e);
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!lb.classList.contains('open')) return;
|
||||
if (e.key === 'Escape') closeLightbox(e);
|
||||
if (e.key === 'ArrowLeft') lbPrev(e);
|
||||
if (e.key === 'ArrowRight') lbNext(e);
|
||||
});
|
||||
return lb;
|
||||
}
|
||||
|
||||
function openLightbox() {
|
||||
const lb = ensureLightbox();
|
||||
const img = document.getElementById('lbImage');
|
||||
img.src = images[currentIndex] || document.getElementById('galleryImage').src;
|
||||
lb.classList.add('open');
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeLightbox(e) {
|
||||
if (e) e.stopPropagation();
|
||||
const lb = document.getElementById('lightbox');
|
||||
if (!lb) return;
|
||||
lb.classList.remove('open');
|
||||
document.documentElement.style.overflow = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function lbSet(index) {
|
||||
currentIndex = (index + images.length) % images.length;
|
||||
const img = document.getElementById('lbImage');
|
||||
if (img) img.src = images[currentIndex];
|
||||
}
|
||||
function lbPrev(e) { if (e) e.stopPropagation(); lbSet(currentIndex - 1); }
|
||||
function lbNext(e) { if (e) e.stopPropagation(); lbSet(currentIndex + 1); }
|
||||
|
||||
// Variant image switching
|
||||
function selectVariant(element) {
|
||||
// Remove selected class from all swatches
|
||||
document.querySelectorAll('.variant-swatch').forEach(s => s.classList.remove('selected'));
|
||||
|
||||
// Add selected class to clicked swatch
|
||||
element.classList.add('selected');
|
||||
|
||||
// Update ring color on the selected dot
|
||||
const variantData = JSON.parse(element.getAttribute('data-variant'));
|
||||
const dot = element.querySelector('.variant-dot');
|
||||
dot.style.boxShadow = `0 0 0 2px white, 0 0 0 3px ${variantData.colorHex}`;
|
||||
|
||||
// Reset other dots
|
||||
document.querySelectorAll('.variant-swatch:not(.selected) .variant-dot').forEach(d => {
|
||||
d.style.boxShadow = '0 0 0 2px white, 0 0 0 3px #ddd';
|
||||
});
|
||||
|
||||
// Update selected variant name display
|
||||
document.getElementById('selectedVariantName').textContent = variantData.colorName;
|
||||
|
||||
// Store selected variant data for cart/wishlist
|
||||
document.getElementById('selectedVariantData').value = JSON.stringify(variantData);
|
||||
|
||||
// Update gallery to highlight variant images
|
||||
if (variantData.images && variantData.images.length > 0) {
|
||||
console.log('Switching to variant images:', variantData.images);
|
||||
const mainImage = document.getElementById('galleryImage');
|
||||
const gallery = document.querySelector('.gallery');
|
||||
const thumbnails = document.querySelectorAll('.gallery-thumbs .thumb');
|
||||
|
||||
// Enable variant filtering mode
|
||||
gallery.classList.add('variant-filtering');
|
||||
|
||||
// Find the first thumbnail that matches this variant's images
|
||||
let firstMatchIndex = -1;
|
||||
thumbnails.forEach((thumb, index) => {
|
||||
const thumbSrc = thumb.getAttribute('data-src');
|
||||
if (variantData.images.includes(thumbSrc)) {
|
||||
thumb.classList.add('variant-match');
|
||||
if (firstMatchIndex === -1) {
|
||||
firstMatchIndex = index;
|
||||
}
|
||||
} else {
|
||||
thumb.classList.remove('variant-match');
|
||||
}
|
||||
});
|
||||
|
||||
// Switch to the first image of this variant
|
||||
if (firstMatchIndex !== -1 && thumbnails[firstMatchIndex]) {
|
||||
const firstThumb = thumbnails[firstMatchIndex];
|
||||
const firstImgSrc = firstThumb.getAttribute('data-src');
|
||||
|
||||
// Remove all active classes
|
||||
thumbnails.forEach(t => t.classList.remove('active'));
|
||||
firstThumb.classList.add('active');
|
||||
|
||||
// Update main image
|
||||
mainImage.src = firstImgSrc;
|
||||
mainImage.alt = variantData.colorName;
|
||||
|
||||
// Update current index for navigation
|
||||
currentIndex = Array.from(thumbnails).indexOf(firstThumb);
|
||||
}
|
||||
|
||||
console.log('Gallery switched to variant, highlighting', variantData.images.length, 'matching images');
|
||||
}
|
||||
|
||||
// Update price if there's a price adjustment
|
||||
if (variantData.priceAdjust && variantData.priceAdjust !== 0) {
|
||||
const basePrice = parseFloat('@Model.Price');
|
||||
const adjustedPrice = basePrice + variantData.priceAdjust;
|
||||
const priceElement = document.querySelector('.product-price .price-value');
|
||||
if (priceElement) {
|
||||
priceElement.textContent = `R ${adjustedPrice.toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Color section toggle (legacy)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const colorSection = document.querySelector('.color-section:not(.variant-section)');
|
||||
const colorTrigger = document.getElementById('colorTrigger');
|
||||
|
||||
if (colorTrigger && colorSection) {
|
||||
colorTrigger.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
colorSection.classList.toggle('show');
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize description collapse functionality
|
||||
initializeDescription();
|
||||
});
|
||||
|
||||
function initializeDescription() {
|
||||
const descContent = document.getElementById('descriptionContent');
|
||||
const seeMoreBtn = document.getElementById('seeMoreBtn');
|
||||
|
||||
if (!descContent || !seeMoreBtn) return;
|
||||
|
||||
// Check if content height exceeds 250px
|
||||
if (descContent.scrollHeight > 250) {
|
||||
descContent.classList.add('collapsed');
|
||||
seeMoreBtn.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDescription() {
|
||||
const descBox = document.getElementById('descriptionBox');
|
||||
const chevron = document.getElementById('descChevron');
|
||||
|
||||
descBox.classList.toggle('closed');
|
||||
|
||||
if (descBox.classList.contains('closed')) {
|
||||
chevron.style.transform = 'rotate(-90deg)';
|
||||
} else {
|
||||
chevron.style.transform = 'rotate(0deg)';
|
||||
// Re-initialize collapsed state when opening
|
||||
initializeDescription();
|
||||
}
|
||||
}
|
||||
|
||||
function expandDescription() {
|
||||
const descContent = document.getElementById('descriptionContent');
|
||||
const seeMoreBtn = document.getElementById('seeMoreBtn');
|
||||
|
||||
if (descContent.classList.contains('collapsed')) {
|
||||
descContent.classList.remove('collapsed');
|
||||
seeMoreBtn.innerHTML = '<i class="bi bi-chevron-up"></i> See Less';
|
||||
} else {
|
||||
descContent.classList.add('collapsed');
|
||||
seeMoreBtn.innerHTML = '<i class="bi bi-chevron-down"></i> See More';
|
||||
// Scroll back to description tab
|
||||
document.querySelector('.description-tab').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
130
Views/Shop/Index.cshtml
Executable file
130
Views/Shop/Index.cshtml
Executable file
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="light">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<meta name="description" content="@ViewData["MetaDescription"] ?? " Sky Art Shop - Scrapbooking, journaling,
|
||||
cardmaking, and collaging stationery."" />
|
||||
<title>SkyArt - @ViewData["Title"]</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="~/assets/css/main.css?v=@DateTime.Now.Ticks" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar">
|
||||
<div class="navbar-content">
|
||||
<div class="nav-brand">
|
||||
<a href="/">
|
||||
<img src="/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg" alt="Logo" class="logo-image" />
|
||||
<h1>@(ViewBag.SiteSettings?.SiteName ?? "Sky Art Shop")</h1>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-center">
|
||||
@await Component.InvokeAsync("Navigation", new { location = "navbar" })
|
||||
</div>
|
||||
|
||||
<div class="nav-icons">
|
||||
<div class="dropdown-container">
|
||||
<a href="#" class="nav-icon" id="wishlistBtn" aria-label="Wishlist">
|
||||
<i class="bi bi-heart"></i>
|
||||
<span class="badge">0</span>
|
||||
</a>
|
||||
<div class="icon-dropdown" id="wishlistDropdown">
|
||||
<div class="dropdown-header">
|
||||
<h4>My Wishlist</h4>
|
||||
</div>
|
||||
<div class="dropdown-items" id="wishlistItems">
|
||||
<p class="empty-message">Your wishlist is empty</p>
|
||||
</div>
|
||||
<div class="dropdown-footer">
|
||||
<a href="/shop" class="btn-view-all">Continue Shopping</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown-container">
|
||||
<a href="#" class="nav-icon" id="cartBtn" aria-label="Cart">
|
||||
<i class="bi bi-cart"></i>
|
||||
<span class="badge">0</span>
|
||||
</a>
|
||||
<div class="icon-dropdown" id="cartDropdown">
|
||||
<div class="dropdown-header">
|
||||
<h4>Shopping Cart</h4>
|
||||
</div>
|
||||
<div class="dropdown-items" id="cartItems">
|
||||
<p class="empty-message">Your cart is empty</p>
|
||||
</div>
|
||||
<div class="dropdown-footer">
|
||||
<div class="dropdown-total">
|
||||
<span>Total:</span>
|
||||
<span id="cartTotal">$0.00</span>
|
||||
</div>
|
||||
<a href="/checkout" class="btn-checkout">Checkout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="nav-toggle" aria-label="Menu" aria-expanded="false">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-dropdown" id="navDropdown">
|
||||
@await Component.InvokeAsync("Navigation", new { location = "dropdown" })
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@RenderBody()
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-brand">
|
||||
<h2>@(ViewBag.SiteSettings?.SiteName ?? "Sky Art Shop")</h2>
|
||||
<p>Follow Us</p>
|
||||
<div class="social-links">
|
||||
<a href="#instagram" aria-label="Instagram">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<h3>Additional Links</h3>
|
||||
@await Component.InvokeAsync("FooterPages")
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>@(ViewBag.SiteSettings?.FooterText ?? "© 2035 by Sky Art Shop. All rights reserved.")</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Force light mode on page load
|
||||
(function() {
|
||||
document.documentElement.setAttribute('data-bs-theme', 'light');
|
||||
document.documentElement.style.colorScheme = 'light';
|
||||
document.body.setAttribute('data-bs-theme', 'light');
|
||||
document.body.style.colorScheme = 'light';
|
||||
})();
|
||||
</script>
|
||||
<script src="~/assets/js/main.js?v=@DateTime.Now.Ticks"></script>
|
||||
<script src="~/assets/js/cart.js?v=@DateTime.Now.Ticks"></script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user