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:
108
Sky_Art_shop/Views/About/Index.cshtml
Normal file
108
Sky_Art_shop/Views/About/Index.cshtml
Normal file
@@ -0,0 +1,108 @@
|
||||
@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 (Model?.ImageGallery != null && Model.ImageGallery.Any())
|
||||
{
|
||||
<div class="about-sidebar">
|
||||
<div class="sidebar-images">
|
||||
@foreach (var image in Model.ImageGallery)
|
||||
{
|
||||
<div class="sidebar-image-item">
|
||||
<img src="@image" alt="Gallery image" />
|
||||
</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>
|
||||
}
|
||||
109
Sky_Art_shop/Views/Admin/Dashboard.cshtml
Normal file
109
Sky_Art_shop/Views/Admin/Dashboard.cshtml
Normal file
@@ -0,0 +1,109 @@
|
||||
@{
|
||||
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">
|
||||
<h5 class="mb-0">System Info</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Site Name:</strong> @ViewBag.SiteName</p>
|
||||
<p><strong>Database:</strong> MongoDB - SkyArtShopDB</p>
|
||||
<p class="mb-0"><strong>Admin:</strong> @ViewBag.AdminEmail</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
103
Sky_Art_shop/Views/Admin/Login.cshtml
Normal file
103
Sky_Art_shop/Views/Admin/Login.cshtml
Normal file
@@ -0,0 +1,103 @@
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Login - Sky Art Shop</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
.login-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
padding: 40px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.login-header h1 {
|
||||
color: #2c3e50;
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.login-header p {
|
||||
color: #7f8c8d;
|
||||
margin: 0;
|
||||
}
|
||||
.form-control {
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
}
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
.btn-login {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.btn-login:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<h1>🛍️ Sky Art Shop</h1>
|
||||
<p>Admin Panel Login</p>
|
||||
</div>
|
||||
|
||||
@if (ViewBag.ErrorMessage != null)
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">
|
||||
@ViewBag.ErrorMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
<form method="post" action="/admin/login">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email Address</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required placeholder="admin@skyartshop.com">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required placeholder="Enter your password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-login">Sign In</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a href="/" class="text-decoration-none" style="color: #667eea;">← Back to Website</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
114
Sky_Art_shop/Views/AdminBlog/Create.cshtml
Normal file
114
Sky_Art_shop/Views/AdminBlog/Create.cshtml
Normal file
@@ -0,0 +1,114 @@
|
||||
@model SkyArtShop.Models.BlogPost
|
||||
@{
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
ViewData["Title"] = "Create Blog Post";
|
||||
}
|
||||
<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>
|
||||
}
|
||||
114
Sky_Art_shop/Views/AdminBlog/Edit.cshtml
Normal file
114
Sky_Art_shop/Views/AdminBlog/Edit.cshtml
Normal file
@@ -0,0 +1,114 @@
|
||||
@model SkyArtShop.Models.BlogPost
|
||||
@{
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
ViewData["Title"] = "Edit Blog Post";
|
||||
}
|
||||
<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>
|
||||
}
|
||||
43
Sky_Art_shop/Views/AdminBlog/Index.cshtml
Normal file
43
Sky_Art_shop/Views/AdminBlog/Index.cshtml
Normal file
@@ -0,0 +1,43 @@
|
||||
@model List<SkyArtShop.Models.BlogPost>
|
||||
@{
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
ViewData["Title"] = "Blog Posts";
|
||||
}
|
||||
<div class="card">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Blog Posts</h5>
|
||||
<a class="btn btn-primary" href="/admin/blog/create">Create Post</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Slug</th>
|
||||
<th>Published</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var post in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>@post.Title</td>
|
||||
<td>@post.Slug</td>
|
||||
<td>@(post.IsPublished ? "Yes" : "No")</td>
|
||||
<td>@post.CreatedAt.ToString("MMM dd, yyyy")</td>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-secondary" href="/admin/blog/edit/@post.Id">Edit</a>
|
||||
<form method="post" action="/admin/blog/delete/@post.Id" class="d-inline" onsubmit="return confirm('Delete this post?');">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
134
Sky_Art_shop/Views/AdminHomepage/CreateSection.cshtml
Normal file
134
Sky_Art_shop/Views/AdminHomepage/CreateSection.cshtml
Normal file
@@ -0,0 +1,134 @@
|
||||
@model SkyArtShop.Models.HomepageSection
|
||||
@{
|
||||
ViewData["Title"] = "Create New Section";
|
||||
Layout = "_AdminLayout";
|
||||
}
|
||||
|
||||
<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>
|
||||
}
|
||||
139
Sky_Art_shop/Views/AdminHomepage/EditSection.cshtml
Normal file
139
Sky_Art_shop/Views/AdminHomepage/EditSection.cshtml
Normal file
@@ -0,0 +1,139 @@
|
||||
@model SkyArtShop.Models.HomepageSection
|
||||
@{
|
||||
ViewData["Title"] = "Edit Section";
|
||||
Layout = "_AdminLayout";
|
||||
}
|
||||
|
||||
<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" 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>
|
||||
@if (!string.IsNullOrEmpty(Model.ImageUrl))
|
||||
{
|
||||
<div class="mb-2">
|
||||
<img src="@Model.ImageUrl" alt="Current image" style="max-width: 300px; max-height: 200px; border: 1px solid #ddd; border-radius: 4px;" />
|
||||
<p class="text-muted small mt-1">Current image (upload a new one to replace)</p>
|
||||
</div>
|
||||
}
|
||||
<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="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>
|
||||
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>
|
||||
}
|
||||
256
Sky_Art_shop/Views/AdminHomepage/Index.cshtml
Normal file
256
Sky_Art_shop/Views/AdminHomepage/Index.cshtml
Normal file
@@ -0,0 +1,256 @@
|
||||
@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) {
|
||||
console.log('Initializing Sortable on:', sortableList);
|
||||
const sortable = Sortable.create(sortableList, {
|
||||
animation: 200,
|
||||
ghostClass: 'sortable-ghost',
|
||||
dragClass: 'sortable-drag',
|
||||
handle: '.drag-handle',
|
||||
draggable: '.section-item',
|
||||
onStart: function(evt) {
|
||||
console.log('Drag started');
|
||||
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;
|
||||
});
|
||||
console.log('Section order updated successfully');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error updating section order:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
console.log('Sortable initialized successfully');
|
||||
} 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>
|
||||
}
|
||||
77
Sky_Art_shop/Views/AdminMenu/Create.cshtml
Normal file
77
Sky_Art_shop/Views/AdminMenu/Create.cshtml
Normal file
@@ -0,0 +1,77 @@
|
||||
@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 asp-for="IsActive" class="form-check-input" type="checkbox" id="IsActive">
|
||||
<label class="form-check-label" for="IsActive">
|
||||
Active (Globally enable this menu item)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input asp-for="ShowInNavbar" class="form-check-input" type="checkbox" id="ShowInNavbar">
|
||||
<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 asp-for="ShowInDropdown" class="form-check-input" type="checkbox" id="ShowInDropdown">
|
||||
<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 asp-for="OpenInNewTab" class="form-check-input" type="checkbox" id="OpenInNewTab">
|
||||
<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>
|
||||
77
Sky_Art_shop/Views/AdminMenu/Edit.cshtml
Normal file
77
Sky_Art_shop/Views/AdminMenu/Edit.cshtml
Normal file
@@ -0,0 +1,77 @@
|
||||
@model SkyArtShop.Models.MenuItem
|
||||
@{
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
ViewData["Title"] = "Edit 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">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="form-check mb-3">
|
||||
<input asp-for="IsActive" class="form-check-input" type="checkbox" id="IsActive">
|
||||
<label class="form-check-label" for="IsActive">
|
||||
Active (Globally enable this menu item)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input asp-for="ShowInNavbar" class="form-check-input" type="checkbox" id="ShowInNavbar">
|
||||
<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 asp-for="ShowInDropdown" class="form-check-input" type="checkbox" id="ShowInDropdown">
|
||||
<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 asp-for="OpenInNewTab" class="form-check-input" type="checkbox" id="OpenInNewTab">
|
||||
<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> Update Menu Item
|
||||
</button>
|
||||
<a href="/admin/menu" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
86
Sky_Art_shop/Views/AdminMenu/Index.cshtml
Normal file
86
Sky_Art_shop/Views/AdminMenu/Index.cshtml
Normal 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">Active</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">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>
|
||||
91
Sky_Art_shop/Views/AdminPages/Create.cshtml
Normal file
91
Sky_Art_shop/Views/AdminPages/Create.cshtml
Normal file
@@ -0,0 +1,91 @@
|
||||
@model SkyArtShop.Models.Page
|
||||
@{
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
ViewData["Title"] = "Create Page";
|
||||
}
|
||||
<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>
|
||||
}
|
||||
447
Sky_Art_shop/Views/AdminPages/Edit.cshtml
Normal file
447
Sky_Art_shop/Views/AdminPages/Edit.cshtml
Normal file
@@ -0,0 +1,447 @@
|
||||
@model SkyArtShop.Models.Page
|
||||
@{
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
ViewData["Title"] = "Edit Page";
|
||||
}
|
||||
<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>
|
||||
}
|
||||
43
Sky_Art_shop/Views/AdminPages/Index.cshtml
Normal file
43
Sky_Art_shop/Views/AdminPages/Index.cshtml
Normal file
@@ -0,0 +1,43 @@
|
||||
@model List<SkyArtShop.Models.Page>
|
||||
@{
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
ViewData["Title"] = "Pages";
|
||||
}
|
||||
<div class="card">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Pages</h5>
|
||||
<a class="btn btn-primary" href="/admin/pages/create">Create Page</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Slug</th>
|
||||
<th>Active</th>
|
||||
<th>Updated</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var p in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>@p.PageName</td>
|
||||
<td>@p.PageSlug</td>
|
||||
<td>@(p.IsActive ? "Yes" : "No")</td>
|
||||
<td>@p.UpdatedAt.ToString("MMM dd, yyyy")</td>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-secondary" href="/admin/pages/edit/@p.Id">Edit</a>
|
||||
<form method="post" action="/admin/pages/delete/@p.Id" class="d-inline" onsubmit="return confirm('Delete this page?');">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
43
Sky_Art_shop/Views/AdminPortfolio/Categories.cshtml
Normal file
43
Sky_Art_shop/Views/AdminPortfolio/Categories.cshtml
Normal file
@@ -0,0 +1,43 @@
|
||||
@model List<SkyArtShop.Models.PortfolioCategory>
|
||||
@{
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
ViewData["Title"] = "Portfolio Categories";
|
||||
}
|
||||
<div class="card">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Categories</h5>
|
||||
<a class="btn btn-primary" href="/admin/portfolio/category/create">Create Category</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Slug</th>
|
||||
<th>Order</th>
|
||||
<th>Active</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var c in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>@c.Name</td>
|
||||
<td>@c.Slug</td>
|
||||
<td>@c.DisplayOrder</td>
|
||||
<td>@(c.IsActive ? "Yes" : "No")</td>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-secondary" href="/admin/portfolio/category/edit/@c.Id">Edit</a>
|
||||
<form method="post" action="/admin/portfolio/category/delete/@c.Id" class="d-inline" onsubmit="return confirm('Delete this category?');">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
81
Sky_Art_shop/Views/AdminPortfolio/CreateCategory.cshtml
Normal file
81
Sky_Art_shop/Views/AdminPortfolio/CreateCategory.cshtml
Normal file
@@ -0,0 +1,81 @@
|
||||
@model SkyArtShop.Models.PortfolioCategory
|
||||
@{
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
ViewData["Title"] = "Create Category";
|
||||
}
|
||||
<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>
|
||||
}
|
||||
87
Sky_Art_shop/Views/AdminPortfolio/CreateProject.cshtml
Normal file
87
Sky_Art_shop/Views/AdminPortfolio/CreateProject.cshtml
Normal file
@@ -0,0 +1,87 @@
|
||||
@model SkyArtShop.Models.PortfolioProject
|
||||
@{
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
ViewData["Title"] = "Create Project";
|
||||
var categories = ViewBag.Categories as List<SkyArtShop.Models.PortfolioCategory> ?? new();
|
||||
}
|
||||
<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" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Category</label>
|
||||
<select class="form-select" name="CategoryId">
|
||||
@foreach (var c in categories)
|
||||
{
|
||||
<option value="@c.Id">@c.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="portfolioDescription" 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>
|
||||
<button class="btn btn-primary" type="submit">Save</button>
|
||||
<a class="btn btn-secondary" href="/admin/portfolio/projects">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="https://cdn.ckeditor.com/ckeditor5/40.1.0/classic/ckeditor.js"></script>
|
||||
<script>
|
||||
let portfolioEditor;
|
||||
ClassicEditor
|
||||
.create(document.querySelector('#portfolioDescription'), {
|
||||
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 => {
|
||||
portfolioEditor = editor;
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
document.querySelector('#portfolioDescription').value = portfolioEditor.getData();
|
||||
});
|
||||
})
|
||||
.catch(error => { console.error(error); });
|
||||
</script>
|
||||
}
|
||||
81
Sky_Art_shop/Views/AdminPortfolio/EditCategory.cshtml
Normal file
81
Sky_Art_shop/Views/AdminPortfolio/EditCategory.cshtml
Normal file
@@ -0,0 +1,81 @@
|
||||
@model SkyArtShop.Models.PortfolioCategory
|
||||
@{
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
ViewData["Title"] = "Edit Category";
|
||||
}
|
||||
<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>
|
||||
}
|
||||
87
Sky_Art_shop/Views/AdminPortfolio/EditProject.cshtml
Normal file
87
Sky_Art_shop/Views/AdminPortfolio/EditProject.cshtml
Normal file
@@ -0,0 +1,87 @@
|
||||
@model SkyArtShop.Models.PortfolioProject
|
||||
@{
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
ViewData["Title"] = "Edit Project";
|
||||
var categories = ViewBag.Categories as List<SkyArtShop.Models.PortfolioCategory> ?? new();
|
||||
}
|
||||
<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" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Category</label>
|
||||
<select class="form-select" name="CategoryId">
|
||||
@foreach (var c in categories)
|
||||
{
|
||||
<option value="@c.Id" selected="@(Model.CategoryId == c.Id ? "selected" : null)">@c.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="portfolioDescription" 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>
|
||||
<button class="btn btn-primary" type="submit">Save</button>
|
||||
<a class="btn btn-secondary" href="/admin/portfolio/projects">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="https://cdn.ckeditor.com/ckeditor5/40.1.0/classic/ckeditor.js"></script>
|
||||
<script>
|
||||
let portfolioEditor;
|
||||
ClassicEditor
|
||||
.create(document.querySelector('#portfolioDescription'), {
|
||||
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 => {
|
||||
portfolioEditor = editor;
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
document.querySelector('#portfolioDescription').value = portfolioEditor.getData();
|
||||
});
|
||||
})
|
||||
.catch(error => { console.error(error); });
|
||||
</script>
|
||||
}
|
||||
58
Sky_Art_shop/Views/AdminPortfolio/Projects.cshtml
Normal file
58
Sky_Art_shop/Views/AdminPortfolio/Projects.cshtml
Normal file
@@ -0,0 +1,58 @@
|
||||
@model List<SkyArtShop.Models.PortfolioProject>
|
||||
@{
|
||||
Layout = "~/Views/Shared/_AdminLayout.cshtml";
|
||||
ViewData["Title"] = "Portfolio Projects";
|
||||
var categories = ViewBag.Categories as List<SkyArtShop.Models.PortfolioCategory> ?? new();
|
||||
var selected = ViewBag.SelectedCategory as string;
|
||||
}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Projects</h5>
|
||||
<a class="btn btn-primary" href="/admin/portfolio/project/create">Create Project</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-2 mb-3">
|
||||
<div class="col-auto">
|
||||
<select name="categoryId" class="form-select" onchange="this.form.submit()">
|
||||
<option value="">All Categories</option>
|
||||
@foreach (var c in categories)
|
||||
{
|
||||
<option value="@c.Id" selected="@(selected == c.Id ? "selected" : null)">@c.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a class="btn btn-secondary" href="/admin/portfolio/projects">Reset</a>
|
||||
</div>
|
||||
</form>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Category</th>
|
||||
<th>Order</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var p in Model)
|
||||
{
|
||||
var catName = categories.FirstOrDefault(c => c.Id == p.CategoryId)?.Name ?? "-";
|
||||
<tr>
|
||||
<td>@p.Title</td>
|
||||
<td>@catName</td>
|
||||
<td>@p.DisplayOrder</td>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-secondary" href="/admin/portfolio/project/edit/@p.Id">Edit</a>
|
||||
<form method="post" action="/admin/portfolio/project/delete/@p.Id" class="d-inline" onsubmit="return confirm('Delete this project?');">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
470
Sky_Art_shop/Views/AdminProducts/Create.cshtml
Normal file
470
Sky_Art_shop/Views/AdminProducts/Create.cshtml
Normal file
@@ -0,0 +1,470 @@
|
||||
@model Product
|
||||
@{
|
||||
ViewData["Title"] = Model?.Id == null ? "Create Product" : "Edit Product";
|
||||
Layout = "_AdminLayout";
|
||||
}
|
||||
|
||||
<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 Colors (Select Multiple)</label>
|
||||
<div id="colorPicker" class="border rounded p-3">
|
||||
@{
|
||||
var availableColors = new[] { "Red", "Blue", "Green", "Yellow", "Orange", "Purple", "Pink", "Black", "White", "Gray", "Brown", "Gold", "Silver", "Multicolor" };
|
||||
var colorHexMap = new Dictionary<string, string> {
|
||||
{"Red", "#FF0000"}, {"Blue", "#0000FF"}, {"Green", "#00FF00"}, {"Yellow", "#FFFF00"},
|
||||
{"Orange", "#FFA500"}, {"Purple", "#800080"}, {"Pink", "#FFC0CB"}, {"Black", "#000000"},
|
||||
{"White", "#FFFFFF"}, {"Gray", "#808080"}, {"Brown", "#A52A2A"}, {"Gold", "#FFD700"},
|
||||
{"Silver", "#C0C0C0"}, {"Multicolor", "linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet)"}
|
||||
};
|
||||
var selectedColors = Model?.Colors ?? new List<string>();
|
||||
}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
@foreach (var color in availableColors)
|
||||
{
|
||||
var isSelected = selectedColors.Contains(color);
|
||||
var bgStyle = color == "Multicolor" ? $"background: {colorHexMap[color]};" : $"background-color: {colorHexMap[color]};";
|
||||
var borderColor = color == "White" || color == "Yellow" ? "border: 2px solid #ccc;" : "border: 2px solid transparent;";
|
||||
|
||||
<div class="form-check color-checkbox" style="margin: 0;">
|
||||
<input class="form-check-input color-input" type="checkbox"
|
||||
name="Colors" value="@color" id="color_@color"
|
||||
@(isSelected ? "checked" : "")
|
||||
style="display: none;">
|
||||
<label class="color-swatch"
|
||||
style="@bgStyle @borderColor width: 40px; height: 40px; border-radius: 50%; cursor: pointer; display: inline-block; position: relative; transition: transform 0.2s;"
|
||||
title="@color"
|
||||
onclick="toggleColorSelection(this)">
|
||||
@if (isSelected)
|
||||
{
|
||||
<i class="bi bi-check-lg" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: @(color == "White" || color == "Yellow" ? "black" : "white"); font-size: 1.5rem; font-weight: bold;"></i>
|
||||
}
|
||||
</label>
|
||||
<small class="d-block text-center mt-1" style="font-size: 0.7rem;">@color</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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-outline-primary btn-sm mt-2 w-100"
|
||||
onclick="document.getElementById('imageUpload').click()">
|
||||
<i class="bi bi-upload"></i> Upload Images (Multiple)
|
||||
</button>
|
||||
<input type="file" id="imageUpload" accept="image/*" multiple style="display: none;" onchange="handleImageUpload(event)">
|
||||
<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);
|
||||
|
||||
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)";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
109
Sky_Art_shop/Views/AdminProducts/Index.cshtml
Normal file
109
Sky_Art_shop/Views/AdminProducts/Index.cshtml
Normal 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">Active</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">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>
|
||||
}
|
||||
69
Sky_Art_shop/Views/AdminSettings/Index.cshtml
Normal file
69
Sky_Art_shop/Views/AdminSettings/Index.cshtml
Normal file
@@ -0,0 +1,69 @@
|
||||
@model SiteSettings
|
||||
@{
|
||||
ViewData["Title"] = "Site Settings";
|
||||
Layout = "_AdminLayout";
|
||||
}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Edit Site Settings</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/admin/settings">
|
||||
<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-6">
|
||||
<div class="mb-3">
|
||||
<label for="SiteName" class="form-label">Site Name</label>
|
||||
<input type="text" class="form-control" id="SiteName" name="SiteName" value="@Model?.SiteName" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="SiteTagline" class="form-label">Site Tagline</label>
|
||||
<input type="text" class="form-control" id="SiteTagline" name="SiteTagline" value="@Model?.SiteTagline">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="ContactEmail" class="form-label">Contact Email</label>
|
||||
<input type="email" class="form-control" id="ContactEmail" name="ContactEmail" value="@Model?.ContactEmail">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="ContactPhone" class="form-label">Contact Phone</label>
|
||||
<input type="text" class="form-control" id="ContactPhone" name="ContactPhone" value="@Model?.ContactPhone">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> <strong>Note:</strong> Homepage content and hero sections are now managed in the <a href="/admin/homepage" class="alert-link">Homepage Editor</a>. Use this page for general site settings only.
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="InstagramUrl" class="form-label">Instagram URL</label>
|
||||
<input type="text" class="form-control" id="InstagramUrl" name="InstagramUrl" value="@Model?.InstagramUrl">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="FooterText" class="form-label">Footer Text</label>
|
||||
<textarea class="form-control" id="FooterText" name="FooterText" rows="2">@Model?.FooterText</textarea>
|
||||
<small class="text-muted">You can also edit the footer in the Homepage Editor</small>
|
||||
</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">Save Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
133
Sky_Art_shop/Views/AdminUpload/Index.cshtml
Normal file
133
Sky_Art_shop/Views/AdminUpload/Index.cshtml
Normal file
@@ -0,0 +1,133 @@
|
||||
@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">
|
||||
<h5 class="card-title">Upload New Image</h5>
|
||||
<form id="uploadForm" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<input type="file" class="form-control" id="imageFile" accept="image/*" multiple>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" onclick="uploadImage()">
|
||||
<i class="bi bi-cloud-upload"></i> Upload Image
|
||||
</button>
|
||||
</form>
|
||||
<div id="uploadProgress" class="mt-3" style="display: none;">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="uploadResult" class="mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Uploaded Images (@Model.Count)</h5>
|
||||
@if (Model.Any())
|
||||
{
|
||||
<div class="row g-3">
|
||||
@foreach (var image in Model)
|
||||
{
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<img src="@image" class="card-img-top" alt="Uploaded image" style="height: 200px; object-fit: cover;">
|
||||
<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>
|
||||
function uploadImage() {
|
||||
const fileInput = document.getElementById('imageFile');
|
||||
const files = fileInput.files;
|
||||
|
||||
if (files.length === 0) {
|
||||
alert('Please select at least one file');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
Array.from(files).forEach(file => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
|
||||
document.getElementById('uploadProgress').style.display = 'block';
|
||||
|
||||
fetch('/admin/upload/multiple', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
document.getElementById('uploadProgress').style.display = 'none';
|
||||
if (result.success) {
|
||||
document.getElementById('uploadResult').innerHTML =
|
||||
'<div class="alert alert-success">Images uploaded successfully!</div>';
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
document.getElementById('uploadResult').innerHTML =
|
||||
'<div class="alert alert-danger">Upload failed: ' + result.message + '</div>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('uploadProgress').style.display = 'none';
|
||||
document.getElementById('uploadResult').innerHTML =
|
||||
'<div class="alert alert-danger">Upload failed: ' + error + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
alert('URL copied to clipboard!');
|
||||
});
|
||||
}
|
||||
|
||||
function deleteImage(imageUrl, button) {
|
||||
if (!confirm('Are you sure you want to delete this image?')) return;
|
||||
|
||||
fetch('/admin/upload/delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(imageUrl)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
button.closest('.col-md-3').remove();
|
||||
} else {
|
||||
alert('Delete failed: ' + result.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
}
|
||||
38
Sky_Art_shop/Views/Blog/Index.cshtml
Normal file
38
Sky_Art_shop/Views/Blog/Index.cshtml
Normal file
@@ -0,0 +1,38 @@
|
||||
@model List<SkyArtShop.Models.BlogPost>
|
||||
@{
|
||||
ViewData["Title"] = "Blog";
|
||||
}
|
||||
|
||||
<section class="portfolio-hero">
|
||||
<div class="container">
|
||||
<h1>Sky Art Shop Blog</h1>
|
||||
<p class="hero-subtitle">Creative ideas, tutorials, and inspiration for your crafting journey</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="shop-products">
|
||||
<div class="container">
|
||||
<div class="projects-grid">
|
||||
@foreach (var post in Model)
|
||||
{
|
||||
<article class="project-card">
|
||||
@if (!string.IsNullOrEmpty(post.FeaturedImage))
|
||||
{
|
||||
<div class="project-image">
|
||||
<img src="@post.FeaturedImage" alt="@post.Title" />
|
||||
</div>
|
||||
}
|
||||
<div class="project-info">
|
||||
<h3>@post.Title</h3>
|
||||
<p class="project-date">@post.CreatedAt.ToString("MMMM dd, yyyy")</p>
|
||||
@if (!string.IsNullOrEmpty(post.Excerpt))
|
||||
{
|
||||
<p>@post.Excerpt</p>
|
||||
}
|
||||
<a href="/blog/post/@post.Slug" class="btn btn-primary btn-small">Read More</a>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
38
Sky_Art_shop/Views/Blog/Post.cshtml
Normal file
38
Sky_Art_shop/Views/Blog/Post.cshtml
Normal file
@@ -0,0 +1,38 @@
|
||||
@model SkyArtShop.Models.BlogPost
|
||||
@{
|
||||
ViewData["Title"] = Model.Title;
|
||||
}
|
||||
|
||||
<section class="portfolio-hero">
|
||||
<div class="container">
|
||||
<h1>@Model.Title</h1>
|
||||
<p class="hero-subtitle">@Model.CreatedAt.ToString("MMMM dd, yyyy")</p>
|
||||
<a href="/blog" class="btn btn-secondary">← Back to Blog</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="shop-products">
|
||||
<div class="container">
|
||||
<article class="blog-post-content">
|
||||
@if (!string.IsNullOrEmpty(Model.FeaturedImage))
|
||||
{
|
||||
<div class="featured-image">
|
||||
<img src="@Model.FeaturedImage" alt="@Model.Title" style="max-width: 100%; height: auto;" />
|
||||
</div>
|
||||
}
|
||||
<div class="content">
|
||||
@Html.Raw(Model.Content)
|
||||
</div>
|
||||
@if (Model.Tags != null && Model.Tags.Any())
|
||||
{
|
||||
<div class="tags">
|
||||
<strong>Tags:</strong>
|
||||
@foreach (var tag in Model.Tags)
|
||||
{
|
||||
<span class="tag">@tag</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
37
Sky_Art_shop/Views/Contact/Index.cshtml
Normal file
37
Sky_Art_shop/Views/Contact/Index.cshtml
Normal file
@@ -0,0 +1,37 @@
|
||||
@{
|
||||
ViewData["Title"] = "Contact";
|
||||
}
|
||||
|
||||
<section class="portfolio-hero">
|
||||
<div class="container">
|
||||
<h1>Contact Us</h1>
|
||||
<p class="hero-subtitle">Get in touch with Sky Art Shop</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="shop-products">
|
||||
<div class="container">
|
||||
<div class="contact-form-wrapper">
|
||||
<partial name="_AdminAlerts" />
|
||||
|
||||
<form method="post" action="/contact/submit" class="contact-form">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="message">Message</label>
|
||||
<textarea id="message" name="message" class="form-control" rows="6" required></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Send Message</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
216
Sky_Art_shop/Views/Home/Index.cshtml
Normal file
216
Sky_Art_shop/Views/Home/Index.cshtml
Normal file
@@ -0,0 +1,216 @@
|
||||
@{
|
||||
ViewData["Title"] = "Home";
|
||||
}
|
||||
|
||||
@if (ViewBag.Sections != null && ViewBag.Sections.Count > 0)
|
||||
{
|
||||
@foreach (var sect in ViewBag.Sections)
|
||||
{
|
||||
@if (sect.SectionType == "hero")
|
||||
{
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<div class="hero-content">
|
||||
<h2>@sect.Title</h2>
|
||||
@if (!string.IsNullOrEmpty(sect.Subtitle))
|
||||
{
|
||||
<p>@sect.Subtitle</p>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(sect.Content))
|
||||
{
|
||||
<div class="hero-description">
|
||||
@Html.Raw(sect.Content)
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(sect.ButtonText) && !string.IsNullOrEmpty(sect.ButtonUrl))
|
||||
{
|
||||
<a href="@sect.ButtonUrl" class="btn btn-primary">@sect.ButtonText</a>
|
||||
}
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(sect.ImageUrl))
|
||||
{
|
||||
<div class="hero-image">
|
||||
<img src="@sect.ImageUrl" alt="@sect.Title" loading="lazy" />
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
else if (sect.SectionType == "inspiration")
|
||||
{
|
||||
<!-- Inspiration Section -->
|
||||
<section class="inspiration">
|
||||
<div class="container">
|
||||
<h2>@sect.Title</h2>
|
||||
<div class="inspiration-content">
|
||||
<div class="inspiration-text">
|
||||
@Html.Raw(sect.Content)
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(sect.ImageUrl))
|
||||
{
|
||||
<div class="inspiration-image">
|
||||
<img src="@sect.ImageUrl" alt="@sect.Title" loading="lazy" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(sect.ButtonText) && !string.IsNullOrEmpty(sect.ButtonUrl))
|
||||
{
|
||||
<a href="@sect.ButtonUrl" class="btn btn-secondary">@sect.ButtonText</a>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
else if (sect.SectionType == "promotion")
|
||||
{
|
||||
<!-- Promotion Section -->
|
||||
<section class="promotion" id="promotion">
|
||||
<div class="container">
|
||||
@Html.Raw(sect.Content)
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
else if (sect.SectionType == "collection")
|
||||
{
|
||||
<!-- Collection Section -->
|
||||
<section class="collection">
|
||||
<div class="container">
|
||||
<h2>@sect.Title</h2>
|
||||
@if (!string.IsNullOrEmpty(sect.Subtitle))
|
||||
{
|
||||
<p class="section-subtitle">@sect.Subtitle</p>
|
||||
}
|
||||
@Html.Raw(sect.Content)
|
||||
@if (!string.IsNullOrEmpty(sect.ButtonText) && !string.IsNullOrEmpty(sect.ButtonUrl))
|
||||
{
|
||||
<a href="@sect.ButtonUrl" class="btn btn-secondary">@sect.ButtonText</a>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
else if (sect.SectionType == "custom")
|
||||
{
|
||||
<!-- Custom Section -->
|
||||
<section class="custom-section">
|
||||
<div class="container">
|
||||
@if (!string.IsNullOrEmpty(sect.Title))
|
||||
{
|
||||
<h2>@sect.Title</h2>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(sect.Subtitle))
|
||||
{
|
||||
<p class="section-subtitle">@sect.Subtitle</p>
|
||||
}
|
||||
@Html.Raw(sect.Content)
|
||||
@if (!string.IsNullOrEmpty(sect.ImageUrl))
|
||||
{
|
||||
<img src="@sect.ImageUrl" alt="@sect.Title" class="img-fluid my-3" loading="lazy" />
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(sect.ButtonText) && !string.IsNullOrEmpty(sect.ButtonUrl))
|
||||
{
|
||||
<a href="@sect.ButtonUrl" class="btn btn-primary mt-3">@sect.ButtonText</a>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Default Hero Section (Fallback) -->
|
||||
<section class="hero">
|
||||
<div class="hero-content">
|
||||
<h2>Scrapbooking and Journaling Fun</h2>
|
||||
<p>Explore the world of creativity and self-expression.</p>
|
||||
<a href="/Shop" class="btn btn-primary">Shop Now</a>
|
||||
</div>
|
||||
<div class="hero-image">
|
||||
<img src="~/assets/images/hero-craft.jpg" alt="Scrapbooking and crafts" loading="lazy" />
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Top Sellers Section -->
|
||||
<section class="top-sellers" id="top-sellers">
|
||||
<div class="container">
|
||||
<h2>Top Sellers</h2>
|
||||
<div class="products-grid">
|
||||
@if (ViewBag.TopProducts != null && ViewBag.TopProducts.Count > 0)
|
||||
{
|
||||
@foreach (var product in ViewBag.TopProducts)
|
||||
{
|
||||
<div class="product-card">
|
||||
<a href="/shop/product/@product.Id" class="product-link">
|
||||
<div class="product-image">
|
||||
@{
|
||||
var imgSrc = !string.IsNullOrEmpty(product.ImageUrl)
|
||||
? product.ImageUrl
|
||||
: (product.Images != null && product.Images.Count > 0
|
||||
? product.Images[0]
|
||||
: "~/assets/images/products/placeholder.jpg");
|
||||
}
|
||||
<img src="@imgSrc" alt="@product.Name" loading="lazy" />
|
||||
</div>
|
||||
<h3>@product.Name</h3>
|
||||
<p class="price">$@product.Price.ToString("F2")</p>
|
||||
</a>
|
||||
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
<button class="btn btn-small btn-icon"
|
||||
onclick="addToWishlist('@product.Id', '@product.Name', @product.Price, '@(product.Images != null && product.Images.Count > 0 ? product.Images[0] : product.ImageUrl ?? "/assets/images/placeholder.jpg")')"
|
||||
aria-label="Add to wishlist">
|
||||
<i class="bi bi-heart"></i>
|
||||
</button>
|
||||
<button class="btn btn-small btn-icon"
|
||||
onclick="addToCart('@product.Id', '@product.Name', @product.Price, '@(product.Images != null && product.Images.Count > 0 ? product.Images[0] : product.ImageUrl ?? "/assets/images/placeholder.jpg")')"
|
||||
aria-label="Add to cart">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path
|
||||
d="M7 4h-2l-1 2h-2v2h2l3.6 7.59-1.35 2.44c-.16.28-.25.61-.25.97 0 1.1.9 2 2 2h12v-2h-11.1c-.14 0-.25-.11-.25-.25l.03-.12.9-1.63h7.42c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.08-.14.12-.31.12-.48 0-.55-.45-1-1-1h-14.31l-.94-2zm3 17c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm8 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="product-card">
|
||||
<div class="product-image">
|
||||
<img src="~/assets/images/products/product-1.jpg" alt="Product 1" loading="lazy" />
|
||||
</div>
|
||||
<h3>Washi Tape Set</h3>
|
||||
<p class="price">$15.99</p>
|
||||
<button class="btn btn-small">Add to Cart</button>
|
||||
</div>
|
||||
<div class="product-card">
|
||||
<div class="product-image">
|
||||
<img src="~/assets/images/products/product-2.jpg" alt="Product 2" loading="lazy" />
|
||||
</div>
|
||||
<h3>Sticker Pack</h3>
|
||||
<p class="price">$8.99</p>
|
||||
<button class="btn btn-small">Add to Cart</button>
|
||||
</div>
|
||||
<div class="product-card">
|
||||
<div class="product-image">
|
||||
<img src="~/assets/images/products/product-3.jpg" alt="Product 3" loading="lazy" />
|
||||
</div>
|
||||
<h3>Journal Bundle</h3>
|
||||
<p class="price">$24.99</p>
|
||||
<button class="btn btn-small">Add to Cart</button>
|
||||
</div>
|
||||
<div class="product-card">
|
||||
<div class="product-image">
|
||||
<img src="~/assets/images/products/product-4.jpg" alt="Product 4" loading="lazy" />
|
||||
</div>
|
||||
<h3>Craft Kit</h3>
|
||||
<p class="price">$32.99</p>
|
||||
<button class="btn btn-small">Add to Cart</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// Cart functionality now loaded from cart.js
|
||||
</script>
|
||||
}
|
||||
23
Sky_Art_shop/Views/Page/View.cshtml
Normal file
23
Sky_Art_shop/Views/Page/View.cshtml
Normal file
@@ -0,0 +1,23 @@
|
||||
@model SkyArtShop.Models.Page
|
||||
@{
|
||||
ViewData["Title"] = Model.Title ?? Model.PageName;
|
||||
ViewData["MetaDescription"] = Model.MetaDescription;
|
||||
}
|
||||
|
||||
<section class="page-hero">
|
||||
<div class="container">
|
||||
<h1>@(Model.Title ?? Model.PageName)</h1>
|
||||
@if (!string.IsNullOrEmpty(Model.Subtitle))
|
||||
{
|
||||
<p class="hero-subtitle">@Model.Subtitle</p>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="page-content">
|
||||
<div class="container">
|
||||
<div class="content-wrapper">
|
||||
@Html.Raw(Model.Content)
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
41
Sky_Art_shop/Views/Portfolio/Category.cshtml
Normal file
41
Sky_Art_shop/Views/Portfolio/Category.cshtml
Normal file
@@ -0,0 +1,41 @@
|
||||
@model List<SkyArtShop.Models.PortfolioProject>
|
||||
@{
|
||||
var category = ViewBag.Category as SkyArtShop.Models.PortfolioCategory;
|
||||
ViewData["Title"] = category?.Name ?? "Portfolio";
|
||||
}
|
||||
|
||||
<section class="portfolio-hero">
|
||||
<div class="container">
|
||||
<h1>@(category?.Name ?? "Portfolio")</h1>
|
||||
@if (!string.IsNullOrEmpty(category?.Description))
|
||||
{
|
||||
<p class="hero-subtitle">@category.Description</p>
|
||||
}
|
||||
<a href="/portfolio" class="btn btn-secondary">← Back to Portfolio</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="shop-products">
|
||||
<div class="container">
|
||||
<div class="projects-grid">
|
||||
@foreach (var project in Model)
|
||||
{
|
||||
<div class="project-card">
|
||||
@if (!string.IsNullOrEmpty(project.FeaturedImage))
|
||||
{
|
||||
<div class="project-image">
|
||||
<img src="@project.FeaturedImage" alt="@project.Title" />
|
||||
</div>
|
||||
}
|
||||
<div class="project-info">
|
||||
<h3>@project.Title</h3>
|
||||
@if (!string.IsNullOrEmpty(project.Description))
|
||||
{
|
||||
<p>@Html.Raw(project.Description)</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
35
Sky_Art_shop/Views/Portfolio/Index.cshtml
Normal file
35
Sky_Art_shop/Views/Portfolio/Index.cshtml
Normal file
@@ -0,0 +1,35 @@
|
||||
@model List<SkyArtShop.Models.PortfolioCategory>
|
||||
@{
|
||||
ViewData["Title"] = "Portfolio";
|
||||
}
|
||||
|
||||
<section class="portfolio-hero">
|
||||
<div class="container">
|
||||
<h1>Sky Art Shop Projects</h1>
|
||||
<p class="hero-subtitle">Welcome to our portfolio. Here you'll find a selection of our work. Explore our projects to learn more about what we do.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="portfolio-gallery">
|
||||
<div class="container">
|
||||
<div class="portfolio-grid">
|
||||
@foreach (var category in Model)
|
||||
{
|
||||
<div class="portfolio-category">
|
||||
<a href="/portfolio/category/@category.Slug" class="category-link">
|
||||
<div class="category-image">
|
||||
<img src="@(string.IsNullOrEmpty(category.FeaturedImage) ? "/assets/images/portfolio/default.jpg" : category.FeaturedImage)" alt="@category.Name" />
|
||||
<div class="category-overlay">
|
||||
<h2>@category.Name</h2>
|
||||
@if (!string.IsNullOrEmpty(category.Description))
|
||||
{
|
||||
<p>@category.Description</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,11 @@
|
||||
@model List<SkyArtShop.Models.Page>
|
||||
|
||||
@if (Model != null && Model.Any())
|
||||
{
|
||||
<ul>
|
||||
@foreach (var p in Model)
|
||||
{
|
||||
<li><a href="/page/@p.PageSlug">@p.PageName</a></li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
@model List<SkyArtShop.Models.MenuItem>
|
||||
|
||||
<ul class="nav-menu">
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
var currentController = ViewContext.RouteData.Values["Controller"]?.ToString();
|
||||
var isActive = item.Url.TrimStart('/').Equals(currentController, StringComparison.OrdinalIgnoreCase) ||
|
||||
(item.Url == "/" && currentController == "Home");
|
||||
|
||||
<li>
|
||||
<a href="@item.Url" class="@(isActive ? "active" : "")" @(item.OpenInNewTab ? "target='_blank'" : "")>
|
||||
@item.Label
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
12
Sky_Art_shop/Views/Shared/_AdminAlerts.cshtml
Normal file
12
Sky_Art_shop/Views/Shared/_AdminAlerts.cshtml
Normal file
@@ -0,0 +1,12 @@
|
||||
@{
|
||||
var success = TempData["SuccessMessage"] as string;
|
||||
var error = TempData["ErrorMessage"] as string;
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(success))
|
||||
{
|
||||
<div class="alert alert-success" role="alert">@success</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">@error</div>
|
||||
}
|
||||
249
Sky_Art_shop/Views/Shared/_AdminLayout.cshtml
Normal file
249
Sky_Art_shop/Views/Shared/_AdminLayout.cshtml
Normal file
@@ -0,0 +1,249 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@ViewData["Title"] - Admin Panel</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
height: 100vh;
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 250px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-track {
|
||||
background: #2c3e50;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb {
|
||||
background: #34495e;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb:hover {
|
||||
background: #3498db;
|
||||
}
|
||||
|
||||
.sidebar .brand {
|
||||
padding: 20px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #34495e;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #2c3e50;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.sidebar nav {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: #ecf0f1;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover {
|
||||
background: #34495e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar .nav-link i {
|
||||
margin-right: 10px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="sidebar" style="height: 100vh; overflow-y: auto; overflow-x: hidden;">
|
||||
<div class="brand">
|
||||
<i class="bi bi-shop"></i> Sky Art Shop
|
||||
</div>
|
||||
<nav class="nav flex-column mt-4">
|
||||
<a class="nav-link @(ViewContext.RouteData.Values["Action"]?.ToString() == "Dashboard" ? "active" : "")"
|
||||
href="/admin/dashboard">
|
||||
<i class="bi bi-speedometer2"></i> Dashboard
|
||||
</a>
|
||||
<hr style="border-color: #34495e; margin: 10px 0;">
|
||||
<div class="px-3 text-muted small mb-2">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>
|
||||
<hr style="border-color: #34495e; margin: 10px 0;">
|
||||
<div class="px-3 text-muted small mb-2">SETTINGS</div>
|
||||
<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>
|
||||
<hr style="border-color: #34495e; margin: 10px 0;">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
|
||||
</html>
|
||||
120
Sky_Art_shop/Views/Shared/_Layout.cshtml
Normal file
120
Sky_Art_shop/Views/Shared/_Layout.cshtml
Normal file
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="@ViewData["MetaDescription"] ?? " Sky Art Shop - Scrapbooking, journaling,
|
||||
cardmaking, and collaging stationery."" />
|
||||
<title>@ViewData["Title"] - @ViewBag.SiteSettings?.SiteName</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 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>
|
||||
81
Sky_Art_shop/Views/Shop/Index.cshtml
Normal file
81
Sky_Art_shop/Views/Shop/Index.cshtml
Normal file
@@ -0,0 +1,81 @@
|
||||
@model List<SkyArtShop.Models.Product>
|
||||
@{
|
||||
ViewData["Title"] = "Shop";
|
||||
var categories = ViewBag.Categories as List<string> ?? new();
|
||||
var selected = ViewBag.SelectedCategory as string;
|
||||
}
|
||||
|
||||
<section class="shop-hero">
|
||||
<div class="container">
|
||||
<h1>Shop All Products</h1>
|
||||
<p class="hero-subtitle">Find everything you need for your creative projects</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="shop-filters">
|
||||
<div class="container">
|
||||
<div class="filter-bar">
|
||||
<div class="filter-group">
|
||||
<label for="category-filter">Category:</label>
|
||||
<select id="category-filter" onchange="window.location.href='/shop?category='+this.value;">
|
||||
<option value="">All Products</option>
|
||||
@foreach (var cat in categories)
|
||||
{
|
||||
<option value="@cat" selected="@(selected == cat ? "selected" : null)">@cat</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="shop-products">
|
||||
<div class="container">
|
||||
<div class="products-grid">
|
||||
@foreach (var product in Model)
|
||||
{
|
||||
<div class="product-card">
|
||||
<a href="/shop/product/@product.Id" class="product-link">
|
||||
<div class="product-image">
|
||||
@{
|
||||
var displayImage = !string.IsNullOrEmpty(product.ImageUrl)
|
||||
? product.ImageUrl
|
||||
: (product.Images != null && product.Images.Count > 0
|
||||
? product.Images[0]
|
||||
: "/assets/images/placeholder.jpg");
|
||||
}
|
||||
<img src="@displayImage" alt="@product.Name" loading="lazy" />
|
||||
</div>
|
||||
<h3>@product.Name</h3>
|
||||
@if (!string.IsNullOrEmpty(product.Color))
|
||||
{
|
||||
<span class="product-color-badge">@product.Color</span>
|
||||
}
|
||||
<div class="product-description">@Html.Raw(product.ShortDescription ?? product.Description)</div>
|
||||
<p class="price">$@product.Price.ToString("F2")</p>
|
||||
</a>
|
||||
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
<button class="btn btn-small btn-icon"
|
||||
onclick="addToWishlist('@product.Id', '@product.Name', @product.Price, '@(product.Images != null && product.Images.Count > 0 ? product.Images[0] : product.ImageUrl ?? "/assets/images/placeholder.jpg")')"
|
||||
aria-label="Add to wishlist">
|
||||
<i class="bi bi-heart"></i>
|
||||
</button>
|
||||
<button class="btn btn-small btn-icon"
|
||||
onclick="addToCart('@product.Id', '@product.Name', @product.Price, '@(product.Images != null && product.Images.Count > 0 ? product.Images[0] : product.ImageUrl ?? "/assets/images/placeholder.jpg")')" aria-label="Add to cart">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path
|
||||
d="M7 4h-2l-1 2h-2v2h2l3.6 7.59-1.35 2.44c-.16.28-.25.61-.25.97 0 1.1.9 2 2 2h12v-2h-11.1c-.14 0-.25-.11-.25-.25l.03-.12.9-1.63h7.42c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.08-.14.12-.31.12-.48 0-.55-.45-1-1-1h-14.31l-.94-2zm3 17c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm8 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// Cart functionality now loaded from cart.js
|
||||
</script>
|
||||
}
|
||||
463
Sky_Art_shop/Views/Shop/Product.cshtml
Normal file
463
Sky_Art_shop/Views/Shop/Product.cshtml
Normal file
@@ -0,0 +1,463 @@
|
||||
@model SkyArtShop.Models.Product
|
||||
@{
|
||||
ViewData["Title"] = Model.Name;
|
||||
}
|
||||
|
||||
<section class="product-detail-modern">
|
||||
<div class="container">
|
||||
<div class="product-split">
|
||||
<!-- LEFT: Gallery -->
|
||||
<div class="image-pane">
|
||||
<div class="gallery">
|
||||
<div class="gallery-sidebar">
|
||||
<div class="gallery-thumbs">
|
||||
@if (Model.Images != null && Model.Images.Count > 0)
|
||||
{
|
||||
@for (int i = 0; i < Model.Images.Count; i++)
|
||||
{
|
||||
var image = Model.Images[i];
|
||||
var isFirst = i == 0;
|
||||
<div class="thumb @(isFirst ? "active" : "")" data-src="@image" onclick="setImage(this)">
|
||||
<img src="@image" alt="@Model.Name">
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.ImageUrl))
|
||||
{
|
||||
<div class="thumb active" data-src="@Model.ImageUrl" onclick="setImage(this)">
|
||||
<img src="@Model.ImageUrl" alt="@Model.Name">
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="thumb active" data-src="/assets/images/placeholder.jpg" onclick="setImage(this)">
|
||||
<img src="/assets/images/placeholder.jpg" alt="@Model.Name">
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="zoom-hint"><i class="bi bi-zoom-in"></i> Click to view full size</div>
|
||||
</div>
|
||||
<div class="gallery-main" onclick="openLightbox()">
|
||||
<button class="nav prev" type="button" onclick="event.stopPropagation(); slideImage(-1)"><i class="bi bi-chevron-left"></i></button>
|
||||
@{
|
||||
var mainImageSrc = Model.Images != null && Model.Images.Count > 0
|
||||
? Model.Images[0]
|
||||
: (!string.IsNullOrEmpty(Model.ImageUrl) ? Model.ImageUrl : "/assets/images/placeholder.jpg");
|
||||
}
|
||||
<img id="galleryImage" src="@mainImageSrc" alt="@Model.Name">
|
||||
<button class="nav next" type="button" onclick="event.stopPropagation(); slideImage(1)"><i class="bi bi-chevron-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: Details -->
|
||||
<div class="info-pane">
|
||||
<div class="details">
|
||||
<h1 class="title">@Model.Name</h1>
|
||||
<div class="meta">
|
||||
<div class="meta-left">
|
||||
@if (!string.IsNullOrEmpty(Model.SKU))
|
||||
{
|
||||
<span class="sku">SKU: @Model.SKU</span>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.Category))
|
||||
{
|
||||
<span class="sku">SKU: @Model.Category.ToUpper().Replace(" ","")@Model.Id?.Substring(Model.Id.Length - 4)</span>
|
||||
}
|
||||
@{
|
||||
var rating = Model.AverageRating > 0 ? Model.AverageRating : 5.0;
|
||||
var fullStars = (int)Math.Floor(rating);
|
||||
var hasHalfStar = (rating - fullStars) >= 0.5;
|
||||
var emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
|
||||
}
|
||||
<div class="stars">
|
||||
@for (int i = 0; i < fullStars; i++)
|
||||
{
|
||||
<i class="bi bi-star-fill"></i>
|
||||
}
|
||||
@if (hasHalfStar)
|
||||
{
|
||||
<i class="bi bi-star-half"></i>
|
||||
}
|
||||
@for (int i = 0; i < emptyStars; i++)
|
||||
{
|
||||
<i class="bi bi-star"></i>
|
||||
}
|
||||
<span class="rating-text">(@Model.TotalReviews review@(Model.TotalReviews != 1 ? "s" : ""))</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.UnitsSold > 0)
|
||||
{
|
||||
<span class="units-sold">@Model.UnitsSold sold</span>
|
||||
}
|
||||
</div>
|
||||
<!-- Price first -->
|
||||
<div class="price-row">
|
||||
<span class="label">Price:</span>
|
||||
<span class="price">$@Model.Price.ToString("F2")</span>
|
||||
</div>
|
||||
<!-- Stock info under price -->
|
||||
<div class="stock-row">
|
||||
@if (Model.StockQuantity > 0)
|
||||
{
|
||||
<div class="stock ok"><i class="bi bi-check-circle-fill"></i> In stock (@Model.StockQuantity+
|
||||
units), ready to be shipped</div>
|
||||
<div class="stock-bar green"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
<!-- Actions below quantity and color -->
|
||||
<div class="actions">
|
||||
@if (Model.StockQuantity > 0)
|
||||
{
|
||||
<button class="cta" onclick="addToCartFromDetail()"><i class="bi bi-cart-plus"></i> Add to Cart</button>
|
||||
<button class="cta alt" onclick="addToWishlistFromDetail()"><i class="bi bi-heart"></i> Add to Wishlist</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="cta" disabled>Out of Stock</button>
|
||||
}
|
||||
</div>
|
||||
<div class="stock bad"><i class="bi bi-x-circle-fill"></i> Out of stock</div>
|
||||
<div class="stock-bar red"></div>
|
||||
}
|
||||
</div>
|
||||
<!-- Quantity next -->
|
||||
<div class="qty-row">
|
||||
<div class="qty-header">
|
||||
<span class="label">Quantity:</span>
|
||||
@if (Model.StockQuantity > 0)
|
||||
{
|
||||
<span class="stock-count">(@Model.StockQuantity available)</span>
|
||||
}
|
||||
</div>
|
||||
<div class="qty">
|
||||
<button type="button" class="qty-btn" onclick="decreaseQuantity()" @(Model.StockQuantity == 0 ?
|
||||
"disabled" : "")><i class="bi bi-dash"></i></button>
|
||||
<input id="quantity" type="number" value="1" min="1" max="@Model.StockQuantity" readonly>
|
||||
<button type="button" class="qty-btn" onclick="increaseQuantity()" @(Model.StockQuantity == 0 ?
|
||||
"disabled" : "")><i class="bi bi-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Actions below quantity -->
|
||||
<div class="actions">
|
||||
@if (Model.StockQuantity > 0)
|
||||
{
|
||||
<button class="cta" onclick="addToCartFromDetail()"><i class="bi bi-cart-plus"></i> Add to Cart</button>
|
||||
<button class="cta alt" onclick="addToWishlistFromDetail()"><i class="bi bi-heart"></i> Add to Wishlist</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="cta" disabled>Out of Stock</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Color picker after actions (still in info pane) -->
|
||||
@{
|
||||
var hasColors = (Model.Colors != null && Model.Colors.Any()) || !string.IsNullOrEmpty(Model.Color);
|
||||
List<string> selectedColors = new List<string>();
|
||||
Dictionary<string, string> colorHexMap = new Dictionary<string, string>();
|
||||
|
||||
if (hasColors)
|
||||
{
|
||||
selectedColors = Model.Colors != null && Model.Colors.Any()
|
||||
? Model.Colors
|
||||
: new List<string> { Model.Color ?? "" };
|
||||
|
||||
colorHexMap = new Dictionary<string, string> {
|
||||
{"Red", "#FF0000"}, {"Blue", "#0000FF"}, {"Green", "#00FF00"}, {"Yellow", "#FFFF00"},
|
||||
{"Orange", "#FFA500"}, {"Purple", "#800080"}, {"Pink", "#FFC0CB"}, {"Black", "#000000"},
|
||||
{"White", "#FFFFFF"}, {"Gray", "#808080"}, {"Brown", "#A52A2A"}, {"Gold", "#FFD700"},
|
||||
{"Silver", "#C0C0C0"}, {"Multicolor", "linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet)"},
|
||||
{"Burgundy", "#800020"}, {"Rust Orange", "#B7410E"}, {"Teal", "#008080"},
|
||||
{"Lime Green", "#32CD32"}, {"Navy Blue", "#000080"}, {"Royal Blue", "#4169E1"},
|
||||
{"Dark Green", "#006400"}, {"Hunter Green", "#355E3B"}
|
||||
};
|
||||
}
|
||||
}
|
||||
@if (hasColors)
|
||||
{
|
||||
<div class="color-section">
|
||||
<div class="color-row" id="colorTrigger">
|
||||
<span class="label">Available Colors:</span>
|
||||
<span class="value">@string.Join(", ", selectedColors)</span>
|
||||
<i class="bi bi-chevron-down color-arrow"></i>
|
||||
</div>
|
||||
<div class="swatches" id="colorSwatches">
|
||||
@foreach (var colorName in selectedColors)
|
||||
{
|
||||
var hexColor = colorHexMap.ContainsKey(colorName) ? colorHexMap[colorName] : "#808080";
|
||||
var isGradient = colorName == "Multicolor";
|
||||
var bgStyle = isGradient ? $"background: {hexColor};" : $"background-color: {hexColor};";
|
||||
|
||||
<div class="swatch active">
|
||||
<span class="dot" style="@bgStyle"></span>
|
||||
<span class="name">@colorName</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.ShortDescription))
|
||||
{
|
||||
<div class="short">
|
||||
@Model.ShortDescription
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.Description))
|
||||
{
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="desc-block">
|
||||
<h3>Description</h3>
|
||||
<div class="content">
|
||||
@Html.Raw(Model.Description)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Product Description Tabs -->
|
||||
|
||||
|
||||
<!-- Related Products Section -->
|
||||
@if (ViewBag.RelatedProducts != null && ViewBag.RelatedProducts.Count > 0)
|
||||
{
|
||||
<div class="row mt-5">
|
||||
<div class="col-12">
|
||||
<h3 class="section-title mb-3">You May Also Like</h3>
|
||||
<p class="text-muted mb-4">Based on what customers are viewing</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="products-grid mb-4">
|
||||
@foreach (var relatedProduct in ViewBag.RelatedProducts)
|
||||
{
|
||||
<div class="product-card">
|
||||
<a href="/shop/product/@relatedProduct.Id" class="product-link">
|
||||
<div class="product-image">
|
||||
<img src="@(string.IsNullOrEmpty(relatedProduct.ImageUrl) ? "/assets/images/placeholder.jpg" : relatedProduct.ImageUrl)"
|
||||
alt="@relatedProduct.Name" loading="lazy">
|
||||
</div>
|
||||
<h3>@relatedProduct.Name</h3>
|
||||
@if (!string.IsNullOrEmpty(relatedProduct.Color))
|
||||
{
|
||||
<span class="product-color-badge">@relatedProduct.Color</span>
|
||||
}
|
||||
<div class="product-description">@Html.Raw(relatedProduct.ShortDescription ?? relatedProduct.Description)</div>
|
||||
<p class="price">$@relatedProduct.Price.ToString("F2")</p>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<a href="/shop?category=@Model.Category" class="btn btn-outline-primary">
|
||||
Browse More @Model.Category Products
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row mt-5">
|
||||
<div class="col-12 text-center">
|
||||
<h3 class="section-title mb-3">Explore Our Collection</h3>
|
||||
<a href="/shop?category=@Model.Category" class="btn btn-outline-primary">
|
||||
Browse @Model.Category
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// Simple slider/gallery with fade transition + hover zoom
|
||||
const images = [
|
||||
@if (Model.Images != null && Model.Images.Count > 0)
|
||||
{
|
||||
@for (int i = 0; i < Model.Images.Count; i++)
|
||||
{
|
||||
@: '@Model.Images[i]'@(i < Model.Images.Count - 1 ? "," : "")
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.ImageUrl))
|
||||
{
|
||||
@: '@Model.ImageUrl'
|
||||
}
|
||||
else
|
||||
{
|
||||
@: '/assets/images/placeholder.jpg'
|
||||
}
|
||||
];
|
||||
let currentIndex = 0;
|
||||
let animating = false;
|
||||
|
||||
function changeImage(nextSrc, direction = 0) {
|
||||
const img = document.getElementById('galleryImage');
|
||||
if (animating) return;
|
||||
animating = true;
|
||||
// small directional nudge for slide feel
|
||||
const shift = direction === 0 ? 0 : (direction > 0 ? 12 : -12);
|
||||
img.style.transform = `translateX(${shift}px) scale(1)`;
|
||||
// start fade-out
|
||||
img.classList.add('fade-out');
|
||||
const onTransitionEnd = () => {
|
||||
img.removeEventListener('transitionend', onTransitionEnd);
|
||||
img.onload = () => {
|
||||
// fade back in once new image is loaded
|
||||
requestAnimationFrame(() => {
|
||||
img.classList.remove('fade-out');
|
||||
img.style.transform = 'scale(1)';
|
||||
animating = false;
|
||||
});
|
||||
};
|
||||
img.src = nextSrc;
|
||||
};
|
||||
// If the browser doesn't fire transitionend (short durations), fallback
|
||||
img.addEventListener('transitionend', onTransitionEnd);
|
||||
// Fallback timeout (safety)
|
||||
setTimeout(() => {
|
||||
if (img.classList.contains('fade-out')) {
|
||||
onTransitionEnd();
|
||||
}
|
||||
}, 220);
|
||||
}
|
||||
|
||||
function setImage(el) {
|
||||
const src = el.getAttribute('data-src');
|
||||
changeImage(src, 0);
|
||||
document.querySelectorAll('.gallery-thumbs .thumb').forEach(t => t.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
currentIndex = images.indexOf(src);
|
||||
}
|
||||
|
||||
function slideImage(direction) {
|
||||
currentIndex = (currentIndex + direction + images.length) % images.length;
|
||||
const nextSrc = images[currentIndex];
|
||||
changeImage(nextSrc, direction);
|
||||
// update active thumb
|
||||
document.querySelectorAll('.gallery-thumbs .thumb').forEach(t => {
|
||||
if (t.getAttribute('data-src') === nextSrc) t.classList.add('active'); else t.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
function increaseQuantity() {
|
||||
const input = document.getElementById('quantity');
|
||||
const max = parseInt(input.max);
|
||||
const current = parseInt(input.value);
|
||||
if (current < max) {
|
||||
input.value = current + 1;
|
||||
}
|
||||
}
|
||||
|
||||
function decreaseQuantity() {
|
||||
const input = document.getElementById('quantity');
|
||||
const current = parseInt(input.value);
|
||||
if (current > 1) {
|
||||
input.value = current - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function addToCartFromDetail() {
|
||||
const quantity = parseInt(document.getElementById('quantity').value);
|
||||
const productId = '@Model.Id';
|
||||
const productName = '@Model.Name';
|
||||
const productPrice = @Model.Price;
|
||||
const imageUrl = '@(Model.Images != null && Model.Images.Count > 0 ? Model.Images[0] : "/assets/images/placeholder.jpg")';
|
||||
|
||||
// Call the cart function multiple times for quantity
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
addToCart(productId, productName, productPrice, imageUrl);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
alert(`Added ${quantity} x ${productName} to cart!`);
|
||||
}
|
||||
|
||||
function addToWishlistFromDetail() {
|
||||
const productId = '@Model.Id';
|
||||
const productName = '@Model.Name';
|
||||
const productPrice = @Model.Price;
|
||||
const imageUrl = '@(Model.Images != null && Model.Images.Count > 0 ? Model.Images[0] : "/assets/images/placeholder.jpg")';
|
||||
|
||||
addToWishlist(productId, productName, productPrice, imageUrl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Lightbox Viewer
|
||||
function ensureLightbox() {
|
||||
let lb = document.getElementById('lightbox');
|
||||
if (lb) return lb;
|
||||
lb = document.createElement('div');
|
||||
lb.id = 'lightbox';
|
||||
lb.className = 'lightbox';
|
||||
lb.innerHTML = `
|
||||
<div class="lightbox-content">
|
||||
<button class="lb-nav lb-prev" type="button" aria-label="Previous" onclick="lbPrev(event)"><i class="bi bi-chevron-left"></i></button>
|
||||
<img id="lbImage" alt="@Model.Name" />
|
||||
<button class="lb-nav lb-next" type="button" aria-label="Next" onclick="lbNext(event)"><i class="bi bi-chevron-right"></i></button>
|
||||
<button class="lb-close" type="button" aria-label="Close" onclick="closeLightbox(event)"><i class="bi bi-x-lg"></i></button>
|
||||
</div>`;
|
||||
document.body.appendChild(lb);
|
||||
lb.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'lightbox') closeLightbox(e);
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!lb.classList.contains('open')) return;
|
||||
if (e.key === 'Escape') closeLightbox(e);
|
||||
if (e.key === 'ArrowLeft') lbPrev(e);
|
||||
if (e.key === 'ArrowRight') lbNext(e);
|
||||
});
|
||||
return lb;
|
||||
}
|
||||
|
||||
function openLightbox() {
|
||||
const lb = ensureLightbox();
|
||||
const img = document.getElementById('lbImage');
|
||||
img.src = images[currentIndex] || document.getElementById('galleryImage').src;
|
||||
lb.classList.add('open');
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeLightbox(e) {
|
||||
if (e) e.stopPropagation();
|
||||
const lb = document.getElementById('lightbox');
|
||||
if (!lb) return;
|
||||
lb.classList.remove('open');
|
||||
document.documentElement.style.overflow = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function lbSet(index) {
|
||||
currentIndex = (index + images.length) % images.length;
|
||||
const img = document.getElementById('lbImage');
|
||||
if (img) img.src = images[currentIndex];
|
||||
}
|
||||
function lbPrev(e) { if (e) e.stopPropagation(); lbSet(currentIndex - 1); }
|
||||
function lbNext(e) { if (e) e.stopPropagation(); lbSet(currentIndex + 1); }
|
||||
|
||||
// Color section toggle
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const colorSection = document.querySelector('.color-section');
|
||||
const colorTrigger = document.getElementById('colorTrigger');
|
||||
|
||||
if (colorTrigger && colorSection) {
|
||||
colorTrigger.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
colorSection.classList.toggle('show');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
3
Sky_Art_shop/Views/_ViewImports.cshtml
Normal file
3
Sky_Art_shop/Views/_ViewImports.cshtml
Normal file
@@ -0,0 +1,3 @@
|
||||
@using SkyArtShop
|
||||
@using SkyArtShop.Models
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
3
Sky_Art_shop/Views/_ViewStart.cshtml
Normal file
3
Sky_Art_shop/Views/_ViewStart.cshtml
Normal file
@@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
||||
Reference in New Issue
Block a user