Fix admin route access and backend configuration

- Added /admin redirect to login page in nginx config
- Fixed backend server.js route ordering for proper admin handling
- Updated authentication middleware and routes
- Added user management routes
- Configured PostgreSQL integration
- Updated environment configuration
This commit is contained in:
Local Server
2025-12-13 22:34:11 -06:00
parent 8bb6430a70
commit 703ab57984
253 changed files with 29870 additions and 157 deletions

View File

@@ -0,0 +1,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>
}

View 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>

View 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>

View 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>
}

View 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>
}

View 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>

View 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>
}

View 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>
}

View 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>
}

View 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>

View 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>

View File

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

View 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>
}

View 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>
}

View 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>

View 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>

View 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>
}

View 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>
}

View 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>
}

View 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>
}

View 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>

View 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>
}

View File

@@ -0,0 +1,109 @@
@model List<Product>
@{
ViewData["Title"] = "Manage Products";
Layout = "_AdminLayout";
}
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">All Products (@Model.Count)</h5>
<a href="/admin/products/create" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add New Product
</a>
</div>
<div class="card">
<div class="card-body">
@if (Model.Any())
{
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Image</th>
<th>Name</th>
<th>Category</th>
<th>Price</th>
<th>Stock</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var product in Model.OrderByDescending(p => p.CreatedAt))
{
<tr>
<td>
@if (!string.IsNullOrEmpty(product.ImageUrl))
{
<img src="@product.ImageUrl" alt="@product.Name" style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;">
}
else
{
<div style="width: 50px; height: 50px; background: #e0e0e0; border-radius: 4px;"></div>
}
</td>
<td>
<strong>@product.Name</strong>
@if (product.IsFeatured)
{
<span class="badge bg-warning text-dark ms-1">Featured</span>
}
@if (product.IsTopSeller)
{
<span class="badge bg-success ms-1">Top Seller</span>
}
</td>
<td>@product.Category</td>
<td>$@product.Price.ToString("F2")</td>
<td>@product.StockQuantity</td>
<td>
@if (product.IsActive)
{
<span class="badge bg-success">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>
}

View 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>

View 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>
}

View 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>

View 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>

View 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>

View 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>
}

View 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>

View 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>

View 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>

View File

@@ -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>
}

View File

@@ -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>

View 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>
}

View 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>

View 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>

View File

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

View File

@@ -0,0 +1,463 @@
@model SkyArtShop.Models.Product
@{
ViewData["Title"] = Model.Name;
}
<section class="product-detail-modern">
<div class="container">
<div class="product-split">
<!-- LEFT: Gallery -->
<div class="image-pane">
<div class="gallery">
<div class="gallery-sidebar">
<div class="gallery-thumbs">
@if (Model.Images != null && Model.Images.Count > 0)
{
@for (int i = 0; i < Model.Images.Count; i++)
{
var image = Model.Images[i];
var isFirst = i == 0;
<div class="thumb @(isFirst ? "active" : "")" data-src="@image" onclick="setImage(this)">
<img src="@image" alt="@Model.Name">
</div>
}
}
else if (!string.IsNullOrEmpty(Model.ImageUrl))
{
<div class="thumb active" data-src="@Model.ImageUrl" onclick="setImage(this)">
<img src="@Model.ImageUrl" alt="@Model.Name">
</div>
}
else
{
<div class="thumb active" data-src="/assets/images/placeholder.jpg" onclick="setImage(this)">
<img src="/assets/images/placeholder.jpg" alt="@Model.Name">
</div>
}
</div>
<div class="zoom-hint"><i class="bi bi-zoom-in"></i> Click to view full size</div>
</div>
<div class="gallery-main" onclick="openLightbox()">
<button class="nav prev" type="button" onclick="event.stopPropagation(); slideImage(-1)"><i class="bi bi-chevron-left"></i></button>
@{
var mainImageSrc = Model.Images != null && Model.Images.Count > 0
? Model.Images[0]
: (!string.IsNullOrEmpty(Model.ImageUrl) ? Model.ImageUrl : "/assets/images/placeholder.jpg");
}
<img id="galleryImage" src="@mainImageSrc" alt="@Model.Name">
<button class="nav next" type="button" onclick="event.stopPropagation(); slideImage(1)"><i class="bi bi-chevron-right"></i></button>
</div>
</div>
</div>
<!-- RIGHT: Details -->
<div class="info-pane">
<div class="details">
<h1 class="title">@Model.Name</h1>
<div class="meta">
<div class="meta-left">
@if (!string.IsNullOrEmpty(Model.SKU))
{
<span class="sku">SKU: @Model.SKU</span>
}
else if (!string.IsNullOrEmpty(Model.Category))
{
<span class="sku">SKU: @Model.Category.ToUpper().Replace(" ","")@Model.Id?.Substring(Model.Id.Length - 4)</span>
}
@{
var rating = Model.AverageRating > 0 ? Model.AverageRating : 5.0;
var fullStars = (int)Math.Floor(rating);
var hasHalfStar = (rating - fullStars) >= 0.5;
var emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
}
<div class="stars">
@for (int i = 0; i < fullStars; i++)
{
<i class="bi bi-star-fill"></i>
}
@if (hasHalfStar)
{
<i class="bi bi-star-half"></i>
}
@for (int i = 0; i < emptyStars; i++)
{
<i class="bi bi-star"></i>
}
<span class="rating-text">(@Model.TotalReviews review@(Model.TotalReviews != 1 ? "s" : ""))</span>
</div>
</div>
@if (Model.UnitsSold > 0)
{
<span class="units-sold">@Model.UnitsSold sold</span>
}
</div>
<!-- Price first -->
<div class="price-row">
<span class="label">Price:</span>
<span class="price">$@Model.Price.ToString("F2")</span>
</div>
<!-- Stock info under price -->
<div class="stock-row">
@if (Model.StockQuantity > 0)
{
<div class="stock ok"><i class="bi bi-check-circle-fill"></i> In stock (@Model.StockQuantity+
units), ready to be shipped</div>
<div class="stock-bar green"></div>
}
else
{
<!-- Actions below quantity and color -->
<div class="actions">
@if (Model.StockQuantity > 0)
{
<button class="cta" onclick="addToCartFromDetail()"><i class="bi bi-cart-plus"></i> Add to Cart</button>
<button class="cta alt" onclick="addToWishlistFromDetail()"><i class="bi bi-heart"></i> Add to Wishlist</button>
}
else
{
<button class="cta" disabled>Out of Stock</button>
}
</div>
<div class="stock bad"><i class="bi bi-x-circle-fill"></i> Out of stock</div>
<div class="stock-bar red"></div>
}
</div>
<!-- Quantity next -->
<div class="qty-row">
<div class="qty-header">
<span class="label">Quantity:</span>
@if (Model.StockQuantity > 0)
{
<span class="stock-count">(@Model.StockQuantity available)</span>
}
</div>
<div class="qty">
<button type="button" class="qty-btn" onclick="decreaseQuantity()" @(Model.StockQuantity == 0 ?
"disabled" : "")><i class="bi bi-dash"></i></button>
<input id="quantity" type="number" value="1" min="1" max="@Model.StockQuantity" readonly>
<button type="button" class="qty-btn" onclick="increaseQuantity()" @(Model.StockQuantity == 0 ?
"disabled" : "")><i class="bi bi-plus"></i></button>
</div>
</div>
<!-- Actions below quantity -->
<div class="actions">
@if (Model.StockQuantity > 0)
{
<button class="cta" onclick="addToCartFromDetail()"><i class="bi bi-cart-plus"></i> Add to Cart</button>
<button class="cta alt" onclick="addToWishlistFromDetail()"><i class="bi bi-heart"></i> Add to Wishlist</button>
}
else
{
<button class="cta" disabled>Out of Stock</button>
}
</div>
<!-- Color picker after actions (still in info pane) -->
@{
var hasColors = (Model.Colors != null && Model.Colors.Any()) || !string.IsNullOrEmpty(Model.Color);
List<string> selectedColors = new List<string>();
Dictionary<string, string> colorHexMap = new Dictionary<string, string>();
if (hasColors)
{
selectedColors = Model.Colors != null && Model.Colors.Any()
? Model.Colors
: new List<string> { Model.Color ?? "" };
colorHexMap = new Dictionary<string, string> {
{"Red", "#FF0000"}, {"Blue", "#0000FF"}, {"Green", "#00FF00"}, {"Yellow", "#FFFF00"},
{"Orange", "#FFA500"}, {"Purple", "#800080"}, {"Pink", "#FFC0CB"}, {"Black", "#000000"},
{"White", "#FFFFFF"}, {"Gray", "#808080"}, {"Brown", "#A52A2A"}, {"Gold", "#FFD700"},
{"Silver", "#C0C0C0"}, {"Multicolor", "linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet)"},
{"Burgundy", "#800020"}, {"Rust Orange", "#B7410E"}, {"Teal", "#008080"},
{"Lime Green", "#32CD32"}, {"Navy Blue", "#000080"}, {"Royal Blue", "#4169E1"},
{"Dark Green", "#006400"}, {"Hunter Green", "#355E3B"}
};
}
}
@if (hasColors)
{
<div class="color-section">
<div class="color-row" id="colorTrigger">
<span class="label">Available Colors:</span>
<span class="value">@string.Join(", ", selectedColors)</span>
<i class="bi bi-chevron-down color-arrow"></i>
</div>
<div class="swatches" id="colorSwatches">
@foreach (var colorName in selectedColors)
{
var hexColor = colorHexMap.ContainsKey(colorName) ? colorHexMap[colorName] : "#808080";
var isGradient = colorName == "Multicolor";
var bgStyle = isGradient ? $"background: {hexColor};" : $"background-color: {hexColor};";
<div class="swatch active">
<span class="dot" style="@bgStyle"></span>
<span class="name">@colorName</span>
</div>
}
</div>
</div>
}
@if (!string.IsNullOrEmpty(Model.ShortDescription))
{
<div class="short">
@Model.ShortDescription
</div>
}
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(Model.Description))
{
<div class="row mt-4">
<div class="col-12">
<div class="desc-block">
<h3>Description</h3>
<div class="content">
@Html.Raw(Model.Description)
</div>
</div>
</div>
</div>
}
<!-- Product Description Tabs -->
<!-- Related Products Section -->
@if (ViewBag.RelatedProducts != null && ViewBag.RelatedProducts.Count > 0)
{
<div class="row mt-5">
<div class="col-12">
<h3 class="section-title mb-3">You May Also Like</h3>
<p class="text-muted mb-4">Based on what customers are viewing</p>
</div>
</div>
<div class="products-grid mb-4">
@foreach (var relatedProduct in ViewBag.RelatedProducts)
{
<div class="product-card">
<a href="/shop/product/@relatedProduct.Id" class="product-link">
<div class="product-image">
<img src="@(string.IsNullOrEmpty(relatedProduct.ImageUrl) ? "/assets/images/placeholder.jpg" : relatedProduct.ImageUrl)"
alt="@relatedProduct.Name" loading="lazy">
</div>
<h3>@relatedProduct.Name</h3>
@if (!string.IsNullOrEmpty(relatedProduct.Color))
{
<span class="product-color-badge">@relatedProduct.Color</span>
}
<div class="product-description">@Html.Raw(relatedProduct.ShortDescription ?? relatedProduct.Description)</div>
<p class="price">$@relatedProduct.Price.ToString("F2")</p>
</a>
</div>
}
</div>
<div class="row">
<div class="col-12 text-center">
<a href="/shop?category=@Model.Category" class="btn btn-outline-primary">
Browse More @Model.Category Products
</a>
</div>
</div>
}
else
{
<div class="row mt-5">
<div class="col-12 text-center">
<h3 class="section-title mb-3">Explore Our Collection</h3>
<a href="/shop?category=@Model.Category" class="btn btn-outline-primary">
Browse @Model.Category
</a>
</div>
</div>
}
</div>
</section>
@section Scripts {
<script>
// Simple slider/gallery with fade transition + hover zoom
const images = [
@if (Model.Images != null && Model.Images.Count > 0)
{
@for (int i = 0; i < Model.Images.Count; i++)
{
@: '@Model.Images[i]'@(i < Model.Images.Count - 1 ? "," : "")
}
}
else if (!string.IsNullOrEmpty(Model.ImageUrl))
{
@: '@Model.ImageUrl'
}
else
{
@: '/assets/images/placeholder.jpg'
}
];
let currentIndex = 0;
let animating = false;
function changeImage(nextSrc, direction = 0) {
const img = document.getElementById('galleryImage');
if (animating) return;
animating = true;
// small directional nudge for slide feel
const shift = direction === 0 ? 0 : (direction > 0 ? 12 : -12);
img.style.transform = `translateX(${shift}px) scale(1)`;
// start fade-out
img.classList.add('fade-out');
const onTransitionEnd = () => {
img.removeEventListener('transitionend', onTransitionEnd);
img.onload = () => {
// fade back in once new image is loaded
requestAnimationFrame(() => {
img.classList.remove('fade-out');
img.style.transform = 'scale(1)';
animating = false;
});
};
img.src = nextSrc;
};
// If the browser doesn't fire transitionend (short durations), fallback
img.addEventListener('transitionend', onTransitionEnd);
// Fallback timeout (safety)
setTimeout(() => {
if (img.classList.contains('fade-out')) {
onTransitionEnd();
}
}, 220);
}
function setImage(el) {
const src = el.getAttribute('data-src');
changeImage(src, 0);
document.querySelectorAll('.gallery-thumbs .thumb').forEach(t => t.classList.remove('active'));
el.classList.add('active');
currentIndex = images.indexOf(src);
}
function slideImage(direction) {
currentIndex = (currentIndex + direction + images.length) % images.length;
const nextSrc = images[currentIndex];
changeImage(nextSrc, direction);
// update active thumb
document.querySelectorAll('.gallery-thumbs .thumb').forEach(t => {
if (t.getAttribute('data-src') === nextSrc) t.classList.add('active'); else t.classList.remove('active');
});
}
function increaseQuantity() {
const input = document.getElementById('quantity');
const max = parseInt(input.max);
const current = parseInt(input.value);
if (current < max) {
input.value = current + 1;
}
}
function decreaseQuantity() {
const input = document.getElementById('quantity');
const current = parseInt(input.value);
if (current > 1) {
input.value = current - 1;
}
}
function addToCartFromDetail() {
const quantity = parseInt(document.getElementById('quantity').value);
const productId = '@Model.Id';
const productName = '@Model.Name';
const productPrice = @Model.Price;
const imageUrl = '@(Model.Images != null && Model.Images.Count > 0 ? Model.Images[0] : "/assets/images/placeholder.jpg")';
// Call the cart function multiple times for quantity
for (let i = 0; i < quantity; i++) {
addToCart(productId, productName, productPrice, imageUrl);
}
// Show success message
alert(`Added ${quantity} x ${productName} to cart!`);
}
function addToWishlistFromDetail() {
const productId = '@Model.Id';
const productName = '@Model.Name';
const productPrice = @Model.Price;
const imageUrl = '@(Model.Images != null && Model.Images.Count > 0 ? Model.Images[0] : "/assets/images/placeholder.jpg")';
addToWishlist(productId, productName, productPrice, imageUrl);
}
// Lightbox Viewer
function ensureLightbox() {
let lb = document.getElementById('lightbox');
if (lb) return lb;
lb = document.createElement('div');
lb.id = 'lightbox';
lb.className = 'lightbox';
lb.innerHTML = `
<div class="lightbox-content">
<button class="lb-nav lb-prev" type="button" aria-label="Previous" onclick="lbPrev(event)"><i class="bi bi-chevron-left"></i></button>
<img id="lbImage" alt="@Model.Name" />
<button class="lb-nav lb-next" type="button" aria-label="Next" onclick="lbNext(event)"><i class="bi bi-chevron-right"></i></button>
<button class="lb-close" type="button" aria-label="Close" onclick="closeLightbox(event)"><i class="bi bi-x-lg"></i></button>
</div>`;
document.body.appendChild(lb);
lb.addEventListener('click', (e) => {
if (e.target.id === 'lightbox') closeLightbox(e);
});
document.addEventListener('keydown', (e) => {
if (!lb.classList.contains('open')) return;
if (e.key === 'Escape') closeLightbox(e);
if (e.key === 'ArrowLeft') lbPrev(e);
if (e.key === 'ArrowRight') lbNext(e);
});
return lb;
}
function openLightbox() {
const lb = ensureLightbox();
const img = document.getElementById('lbImage');
img.src = images[currentIndex] || document.getElementById('galleryImage').src;
lb.classList.add('open');
document.documentElement.style.overflow = 'hidden';
document.body.style.overflow = 'hidden';
}
function closeLightbox(e) {
if (e) e.stopPropagation();
const lb = document.getElementById('lightbox');
if (!lb) return;
lb.classList.remove('open');
document.documentElement.style.overflow = '';
document.body.style.overflow = '';
}
function lbSet(index) {
currentIndex = (index + images.length) % images.length;
const img = document.getElementById('lbImage');
if (img) img.src = images[currentIndex];
}
function lbPrev(e) { if (e) e.stopPropagation(); lbSet(currentIndex - 1); }
function lbNext(e) { if (e) e.stopPropagation(); lbSet(currentIndex + 1); }
// Color section toggle
document.addEventListener('DOMContentLoaded', function() {
const colorSection = document.querySelector('.color-section');
const colorTrigger = document.getElementById('colorTrigger');
if (colorTrigger && colorSection) {
colorTrigger.addEventListener('click', function(e) {
e.preventDefault();
colorSection.classList.toggle('show');
});
}
});
</script>
}

View File

@@ -0,0 +1,3 @@
@using SkyArtShop
@using SkyArtShop.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}