updateweb

This commit is contained in:
Local Server
2025-12-14 01:54:40 -06:00
parent dce6460994
commit 61929a5daf
454 changed files with 12193 additions and 42002 deletions

60
ACCESS_FROM_WINDOWS.md Normal file
View File

@@ -0,0 +1,60 @@
# How to Access from Windows
## The Problem
When you type "localhost" in Firefox on Windows, you're accessing WINDOWS' localhost, not the Linux server!
The Linux server has NO website on port 80 (it's completely deleted).
## The Solution
### Option 1: Use Port 5000 (Recommended for Development)
```
http://localhost:5000/
```
### Option 2: Use the Server's IP Address
```
http://192.168.10.130:5000/
```
### Option 3: Setup Port Forwarding (If you really want port 80)
On Windows, open PowerShell as Administrator and run:
```powershell
netsh interface portproxy add v4tov4 listenport=80 listenaddress=0.0.0.0 connectport=5000 connectaddress=192.168.10.130
```
Then you can use:
```
http://localhost/
```
To remove it later:
```powershell
netsh interface portproxy delete v4tov4 listenport=80 listenaddress=0.0.0.0
```
## Why This Happens
- **Windows**: Has its own "localhost" (127.0.0.1)
- **Linux Server**: Different machine at 192.168.10.130
- **Firefox on Windows**: Looks at Windows localhost, not Linux
## Correct URLs
### ✅ CORRECT:
- `http://localhost:5000/` (Windows forwards to Linux)
- `http://192.168.10.130:5000/` (Direct to Linux)
### ❌ WRONG:
- `http://localhost/` (This is Windows localhost, not Linux!)
- `http://localhost:80/` (Same problem)
## Quick Test
Open Firefox and try BOTH:
1. `http://localhost:5000/` - Should work
2. `http://192.168.10.130:5000/` - Should work
If neither works, there might be a firewall blocking port 5000.

357
ADMIN_QUICK_REFERENCE.md Normal file
View File

@@ -0,0 +1,357 @@
# Quick Reference - Admin Panel Usage Guide
## 🚀 Getting Started
### Login to Admin Panel
```
URL: http://localhost:5000/admin/login.html
```
## 📊 Admin Sections Overview
### 1. **Dashboard** (`/admin/dashboard.html`)
- View statistics (products, projects, blog posts, pages count)
- Quick access tiles to all sections
- **Features:** Live stats, quick navigation
### 2. **Homepage Editor** (`/admin/homepage.html`)
- Configure homepage sections
- Enable/disable hero, promotion, portfolio sections
- Set titles, descriptions, CTAs
- **Publishes to:** `/api/homepage/settings`
### 3. **Products** (`/admin/products.html`)
- ✅ Create new products
- ✅ Edit existing products
- ✅ Delete products
- ✅ Set active/inactive status
- ✅ Mark as bestseller
- **Publishes to:** `/api/products` (only active products)
### 4. **Portfolio** (`/admin/portfolio.html`)
- ✅ Add portfolio projects
- ✅ Edit projects
- ✅ Delete projects
- ✅ Set active/inactive status
- ✅ Categorize projects
- **Publishes to:** `/api/portfolio/projects` (only active projects)
### 5. **Blog** (`/admin/blog.html`)
- ✅ Create blog posts
- ✅ Edit posts
- ✅ Delete posts
- ✅ Publish/unpublish
- ✅ Auto-generate slugs
- ✅ SEO meta fields
- **Publishes to:** `/api/blog/posts` (only published posts)
### 6. **Custom Pages** (`/admin/pages.html`)
- ✅ Create custom pages
- ✅ Edit page content
- ✅ Delete pages
- ✅ Set active/inactive
- ✅ Custom slugs
- ✅ SEO optimization
- **Publishes to:** `/api/pages` (only active pages)
### 7. **Menu** (`/admin/menu.html`)
- ✅ Add menu items
- ✅ Edit menu items
- ✅ Reorder via drag-and-drop
- ✅ Show/hide items
- ✅ Set custom icons
- **Publishes to:** `/api/menu` (only visible items)
### 8. **Settings** (`/admin/settings.html`)
- Configure site name, tagline
- Set contact information
- Timezone settings
- Homepage display options
- **Publishes to:** `/api/settings`
### 9. **Users** (`/admin/users.html`)
- ✅ Create admin users
- ✅ Edit user accounts
- ✅ Change passwords
- ✅ Activate/deactivate users
- ✅ Assign roles (Cashier, Accountant, Admin, MasterAdmin)
- View user permissions
## 🔄 Content Publishing Workflow
### Step-by-Step: Publishing a Product
1. **Login** → Dashboard → **Products**
2. Click **"Add New Product"**
3. Fill in details:
- Name (required)
- Description
- Price (required)
- Stock quantity
- Category
- Toggle **"Active"** = ON ✅
- Toggle **"Best Seller"** (optional)
4. Click **"Save & Publish"**
5. ✅ Product is now live on frontend at `/api/products`
### Step-by-Step: Publishing a Blog Post
1. **Login** → Dashboard → **Blog**
2. Click **"Create Blog Post"**
3. Fill in:
- Title (auto-generates slug)
- Slug (customizable)
- Excerpt
- Content
- Meta title & description (SEO)
- Toggle **"Published"** = ON ✅
4. Click **"Save & Publish"**
5. ✅ Post appears at `/api/blog/posts` and `/api/blog/posts/:slug`
### Step-by-Step: Creating a Custom Page
1. **Login** → Dashboard → **Custom Pages**
2. Click **"Create Custom Page"**
3. Enter:
- Title
- Slug (URL-friendly name)
- Content (full HTML supported)
- Meta title & description
- Toggle **"Active"** = ON ✅
4. Click **"Save & Publish"**
5. ✅ Page accessible at `/api/pages/:slug`
## 🔐 Authentication & Session
### Session Details
- **Duration:** 24 hours
- **Storage:** PostgreSQL database
- **Cookie Name:** `skyartshop.sid`
- **Auto-logout:** After 24 hours of inactivity
### Troubleshooting Login Issues
```bash
# Clear session data
DELETE FROM session WHERE expire < NOW();
# Restart backend
pm2 restart skyartshop
# Clear browser cookies
# In browser: DevTools → Application → Cookies → Clear
```
## 📡 API Endpoints Reference
### Admin API (Requires Authentication)
```
POST /api/admin/login
GET /api/admin/session
POST /api/admin/logout
GET /api/admin/dashboard/stats
GET /api/admin/products
POST /api/admin/products
PUT /api/admin/products/:id
DELETE /api/admin/products/:id
GET /api/admin/portfolio/projects
POST /api/admin/portfolio/projects
PUT /api/admin/portfolio/projects/:id
DELETE /api/admin/portfolio/projects/:id
GET /api/admin/blog
POST /api/admin/blog
PUT /api/admin/blog/:id
DELETE /api/admin/blog/:id
GET /api/admin/pages
POST /api/admin/pages
PUT /api/admin/pages/:id
DELETE /api/admin/pages/:id
GET /api/admin/menu
POST /api/admin/menu
GET /api/admin/settings
POST /api/admin/settings
GET /api/admin/homepage/settings
POST /api/admin/homepage/settings
```
### Public API (No Authentication)
```
GET /api/products - Active products
GET /api/products/featured - Featured products
GET /api/products/:id - Single product
GET /api/portfolio/projects - Active portfolio projects
GET /api/blog/posts - Published blog posts
GET /api/blog/posts/:slug - Single blog post
GET /api/pages - Active custom pages
GET /api/pages/:slug - Single custom page
GET /api/menu - Visible menu items
GET /api/homepage/settings - Homepage configuration
GET /api/settings - Public site settings
```
## 🎨 Publishing to Frontend
### How Content Flows
```
Admin Panel → Database (with status flag) → Public API → Frontend Display
```
### Status Flags
- **Products:** `isactive = true`
- **Portfolio:** `isactive = true`
- **Blog:** `ispublished = true`
- **Pages:** `isactive = true`
- **Menu:** `visible = true`
### Frontend Integration Example
```javascript
// Fetch products on shop page
fetch('/api/products')
.then(res => res.json())
.then(data => {
// data.products contains all active products
renderProducts(data.products);
});
// Fetch blog posts
fetch('/api/blog/posts')
.then(res => res.json())
.then(data => {
// data.posts contains all published posts
renderBlogPosts(data.posts);
});
```
## 🛠️ Common Tasks
### Adding a New Product
```
1. Products → Add New Product
2. Fill: Name, Description, Price, Stock
3. Toggle Active = ON
4. Save & Publish
✅ Appears on /api/products
```
### Creating Blog Content
```
1. Blog → Create Blog Post
2. Enter: Title, Content, Excerpt
3. Toggle Published = ON
4. Save & Publish
✅ Appears on /api/blog/posts
```
### Building Navigation Menu
```
1. Menu → Add Menu Item
2. Enter: Label, URL, Icon (optional)
3. Toggle Visible = ON
4. Drag to reorder
5. Save Order
✅ Appears on /api/menu
```
### Configuring Homepage
```
1. Homepage Editor
2. Enable/disable sections
3. Set titles, descriptions, CTAs
4. Upload images (if applicable)
5. Save Changes
✅ Updates /api/homepage/settings
```
## 📋 Testing Checklist
After making changes, verify:
- [ ] Content appears in admin panel list
- [ ] Content is marked as active/published
- [ ] Public API returns the content (`curl http://localhost:5000/api/...`)
- [ ] Frontend displays the new content
- [ ] Session persists when navigating between sections
- [ ] No console errors in browser DevTools
## 🚨 Troubleshooting
### "Getting Logged Out When Clicking Navigation"
**Fixed!** All pages now use shared authentication (auth.js)
### "Content Not Appearing on Frontend"
Check:
1. Is content marked as Active/Published in admin?
2. Test public API: `curl http://localhost:5000/api/products`
3. Check browser console for errors
4. Verify database record has `isactive=true` or `ispublished=true`
### "Changes Not Saving"
1. Check browser console for errors
2. Verify session is active (look for 401 errors)
3. Try logging out and back in
4. Check backend logs: `pm2 logs skyartshop`
### "API Returns Empty Array"
Normal if no content has been created yet. Add content via admin panel.
## 📞 Support Commands
```bash
# Restart backend
pm2 restart skyartshop
# View backend logs
pm2 logs skyartshop
# Check backend status
pm2 status
# Test all endpoints
cd /media/pts/Website/SkyArtShop/backend
./test-navigation.sh
# Clear sessions
psql -d skyartshop -c "DELETE FROM session WHERE expire < NOW();"
```
---
**Last Updated:** December 13, 2025
**Version:** 1.0.0
**Status:** ✅ Fully Operational

164
CLEANUP_COMPLETE.md Normal file
View File

@@ -0,0 +1,164 @@
# Website Cleanup Complete - December 14, 2025
## 🎯 Cleanup Summary
### Space Saved: **~12.2 GB**
## What Was Deleted
### 1. Massive Old Build Directories (~12GB)
-`bin/` - 12GB of .NET build artifacts
-`obj/` - 417MB of .NET compilation files
- **Result**: Freed 12.4GB of disk space
### 2. Old .NET MVC Project (175MB)
-`Sky_Art_shop/` - Old ASP.NET Core project
-`Controllers/` - Old MVC controllers
-`Data/` - Old data models
-`Models/` - Old entity models
-`Services/` - Old service layer
-`ViewComponents/` - Old view components
-`Views/` - Old Razor views
-`variant-api/` - Old API variant
-`publish/` - Old publish folder
- **Result**: Removed entire legacy .NET project
### 3. Broken Files
-`website/public/home-new.html` - Had PHP code that wouldn't work
-`.csproj`, `.sln` files - .NET project files
-`appsettings*.json` - Old .NET config files
### 4. Old Setup Scripts (Archived)
Moved to `backend/old-setup-scripts/`:
- check-ports.sh, check-status.sh, check-system.sh
- complete-setup.sh, create-server.sh, create-temp-admin.js
- create-views.sh, final-test.sh
- generate-hash.js, generate-password.js
- https-status.sh, setup-*.sh, setup-*.sql
- admin-panel-schema.sql, quick-setup.sql, test-login.js
### 5. Old Documentation (Archived)
Moved to `old-docs/`:
- ADMIN_NAVIGATION_FIX.md
- ADMIN_NAVIGATION_SESSION_FIX.md
- ADMIN_PANEL_IMPLEMENTATION_COMPLETE.md
- COLOR-VARIANT-SOLUTION.md
- COMPLETE_UPGRADE_SUMMARY.md
- DEPLOYMENT_FIX_COMPLETE.md
- DUAL_SITE_FIX_COMPLETE.md
- FRONTEND_BACKEND_SYNC_GUIDE.md
- FRONTEND_COMPLETE.md
- RESTORATION_COMPLETE.md
- WEBSITE_CONSOLIDATION_COMPLETE.md
## 📦 Backups Created
### Safety First
- `old-backups/dotnet-project-backup-20251214.tar.gz` - Full backup of .NET project
- `backend/old-setup-scripts/` - All setup scripts preserved
- `old-docs/` - All old documentation preserved
## ✅ What Remains (Clean & Working)
### Current Structure (177MB total)
```
SkyArtShop/
├── website/ # Active website
│ ├── public/ # 9 public HTML pages
│ ├── admin/ # 10 admin HTML pages
│ ├── assets/ # CSS, JS, images
│ └── uploads/ # User uploads
├── backend/ # Node.js backend
│ ├── server.js # Main server
│ ├── routes/ # API routes
│ ├── config/ # Configuration
│ ├── middleware/ # Auth middleware
│ └── old-setup-scripts/ # Archived scripts
├── wwwroot/ # Static assets
├── nginx-skyartshop-*.conf # Nginx configs
├── deploy-*.sh # Deployment scripts
├── dev-start.sh # Development helper
├── test-*.sh # Testing scripts
├── verify-*.sh # Verification scripts
└── Documentation:
├── DEVELOPMENT_MODE.md # Current dev guide
├── ADMIN_QUICK_REFERENCE.md
├── WORKFLOW.md
└── GIT-README.md
```
### File Counts
- **HTML files**: 18 (9 public + 9 admin)
- **JavaScript files**: 12 (8 admin JS + 4 other)
- **CSS files**: 4
- **Backend routes**: Working API endpoints
- **Documentation**: 4 essential guides
## 🧪 Post-Cleanup Testing
### All Tests Passed ✅
1. **Backend Server**: ✅ Running (PM2: online)
2. **Homepage**: ✅ Loads correctly
3. **Admin Login**: ✅ Loads correctly
4. **API Health**: ✅ Database connected
5. **Website Structure**: ✅ All files intact
6. **Port 5000**: ✅ Active and serving
### Test Results
```bash
✅ http://localhost:5000/ - Working
✅ http://localhost:5000/admin/ - Working
✅ http://localhost:5000/health - Working
✅ API endpoints - Working
✅ Static files - Working
```
## 📊 Before & After
### Disk Space
- **Before**: ~12.5 GB
- **After**: 177 MB
- **Savings**: 12.3 GB (98.6% reduction!)
### Project Complexity
- **Before**: Mixed .NET + Node.js project, 31+ documentation files
- **After**: Clean Node.js project, 4 essential docs
- **Result**: Simplified, focused, maintainable
### Performance
- **Load Time**: Faster (less disk I/O)
- **Clarity**: Much clearer structure
- **Maintenance**: Easier to navigate
## 🎉 Benefits
1. **Massive Space Savings**: Freed 12.3GB of disk space
2. **Cleaner Structure**: Removed all legacy .NET code
3. **Easier Navigation**: Clear, focused directory structure
4. **Better Performance**: Less clutter, faster operations
5. **Safer**: All old files backed up before deletion
6. **Simpler Maintenance**: Only relevant files remain
## 🚀 Next Steps
Your development environment is now:
- ✅ Clean and organized
- ✅ 12GB lighter
- ✅ Easy to understand
- ✅ Ready for development
### Start Developing
```bash
./dev-start.sh # Check status
http://localhost:5000 # Access your site
```
### If You Need Old Files
- .NET project backup: `old-backups/dotnet-project-backup-20251214.tar.gz`
- Setup scripts: `backend/old-setup-scripts/`
- Old docs: `old-docs/`
---
**Status**: ✅ Cleanup Complete - Website tested and working perfectly!

View File

@@ -1,59 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
public class AboutController : Controller
{
private readonly PostgreSQLService _pgService;
private readonly string _pagesCollection = "Pages";
public AboutController(PostgreSQLService pgService)
{
_pgService = pgService;
}
public async Task<IActionResult> Index()
{
Page page = (await _pgService.GetAllAsync<Page>(_pagesCollection)).FirstOrDefault((Page p) => p.PageSlug == "about" && p.IsActive);
Console.WriteLine($"[ABOUT] Found About page: {page != null}");
if (page != null)
{
Console.WriteLine("[ABOUT] Title: " + page.Title);
Console.WriteLine($"[ABOUT] Content length: {page.Content?.Length ?? 0}");
Console.WriteLine($"[ABOUT] Image Gallery Count: {page.ImageGallery?.Count ?? 0}");
Console.WriteLine($"[ABOUT] Team Members Count: {page.TeamMembers?.Count ?? 0}");
if (page.ImageGallery != null && page.ImageGallery.Any())
{
Console.WriteLine("[ABOUT] Gallery Images: " + string.Join(", ", page.ImageGallery));
}
if (page.TeamMembers != null && page.TeamMembers.Any())
{
foreach (TeamMember teamMember in page.TeamMembers)
{
Console.WriteLine($"[ABOUT] Team: {teamMember.Name} ({teamMember.Role}) - Photo: {teamMember.PhotoUrl}");
}
}
}
if (page == null)
{
page = new Page
{
PageName = "About",
PageSlug = "about",
Title = "About Sky Art Shop",
Subtitle = "Creating moments, one craft at a time",
Content = "<h2>Our Story</h2><p>Sky Art Shop specializes in scrapbooking, journaling, cardmaking, and collaging stationery.</p>",
ImageGallery = new List<string>(),
TeamMembers = new List<TeamMember>()
};
}
return View(page);
}
}

View File

@@ -1,93 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
[Route("admin/blog")]
[Authorize(Roles = "Admin,MasterAdmin")]
public class AdminBlogController : Controller
{
private readonly PostgreSQLService _pgService;
private readonly SlugService _slugService;
private readonly string _blogCollection = "BlogPosts";
public AdminBlogController(PostgreSQLService pgService, SlugService slugService)
{
_pgService = pgService;
_slugService = slugService;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
return View((await _pgService.GetAllAsync<BlogPost>(_blogCollection)).OrderByDescending((BlogPost p) => p.CreatedAt).ToList());
}
[HttpGet("create")]
public IActionResult Create()
{
return View(new BlogPost());
}
[HttpPost("create")]
public async Task<IActionResult> Create(BlogPost post)
{
if (!base.ModelState.IsValid)
{
return View(post);
}
post.CreatedAt = DateTime.UtcNow;
post.UpdatedAt = DateTime.UtcNow;
post.PublishedDate = DateTime.UtcNow;
post.Slug = _slugService.GenerateSlug(post.Title);
await _pgService.InsertAsync(_blogCollection, post);
base.TempData["SuccessMessage"] = "Blog post created successfully!";
return RedirectToAction("Index");
}
[HttpGet("edit/{id}")]
public async Task<IActionResult> Edit(string id)
{
BlogPost blogPost = await _pgService.GetByIdAsync<BlogPost>(_blogCollection, id);
if (blogPost == null)
{
return NotFound();
}
return View(blogPost);
}
[HttpPost("edit/{id}")]
public async Task<IActionResult> Edit(string id, BlogPost post)
{
if (!base.ModelState.IsValid)
{
return View(post);
}
post.Id = id;
post.UpdatedAt = DateTime.UtcNow;
post.Slug = _slugService.GenerateSlug(post.Title);
await _pgService.UpdateAsync(_blogCollection, id, post);
base.TempData["SuccessMessage"] = "Blog post updated successfully!";
return RedirectToAction("Index");
}
[HttpPost("delete/{id}")]
public async Task<IActionResult> Delete(string id)
{
await _pgService.DeleteAsync<BlogPost>(_blogCollection, id);
base.TempData["SuccessMessage"] = "Blog post deleted successfully!";
return RedirectToAction("Index");
}
private string GenerateSlug(string text)
{
return _slugService.GenerateSlug(text);
}
}

View File

@@ -1,203 +0,0 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
[Route("admin")]
[Authorize(Roles = "Admin,MasterAdmin,Cashier,Accountant")]
public class AdminController : Controller
{
private readonly PostgreSQLService _pgService;
private readonly PostgreAuthService _pgAuthService;
public AdminController(PostgreSQLService pgService, PostgreAuthService pgAuthService)
{
_pgService = pgService;
_pgAuthService = pgAuthService;
}
[HttpGet("login")]
[AllowAnonymous]
public IActionResult Login()
{
IIdentity? identity = base.User.Identity;
if (identity != null && identity.IsAuthenticated)
{
return RedirectToAction("Dashboard");
}
return View();
}
[HttpPost("login")]
[AllowAnonymous]
public async Task<IActionResult> Login(string email, string password)
{
AdminUser adminUser = await _pgAuthService.AuthenticateAsync(email, password);
if (adminUser == null)
{
base.ViewBag.Error = "Invalid email or password";
return View();
}
ClaimsPrincipal principal = _pgAuthService.CreateClaimsPrincipal(adminUser);
await base.HttpContext.SignInAsync("Cookies", principal, new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(30.0)
});
return RedirectToAction("Dashboard");
}
[HttpGet("logout")]
public async Task<IActionResult> Logout()
{
await base.HttpContext.SignOutAsync("Cookies");
return RedirectToAction("Login");
}
[HttpGet("dashboard")]
public async Task<IActionResult> Dashboard()
{
List<Product> products = await _pgService.GetAllAsync<Product>("Products");
List<PortfolioProject> projects = await _pgService.GetAllAsync<PortfolioProject>("PortfolioProjects");
List<BlogPost> blogPosts = await _pgService.GetAllAsync<BlogPost>("BlogPosts");
List<Page> pages = await _pgService.GetAllAsync<Page>("Pages");
SiteSettings siteSettings = await _pgService.GetSiteSettingsAsync();
base.ViewBag.ProductCount = products.Count;
base.ViewBag.ProjectCount = projects.Count;
base.ViewBag.BlogCount = blogPosts.Count;
base.ViewBag.PageCount = pages.Count;
base.ViewBag.SiteName = siteSettings?.SiteName ?? "Sky Art Shop";
base.ViewBag.AdminEmail = base.User.Identity?.Name;
return View();
}
[HttpGet("")]
public IActionResult Index()
{
return RedirectToAction("Dashboard");
}
[HttpGet("system-status")]
[Authorize]
public async Task<IActionResult> SystemStatus()
{
try
{
var value = new
{
databaseConnected = true,
dbType = "PostgreSQL",
dbHost = "localhost",
dbName = "skyartshop",
dbVersion = "16",
userCount = (await _pgService.GetAllAsync<AdminUser>("AdminUsers")).Count,
timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss UTC")
};
return new JsonResult(value);
}
catch (Exception ex)
{
var value2 = new
{
databaseConnected = false,
error = ex.Message,
timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss UTC")
};
return new JsonResult(value2);
}
}
[HttpGet("change-password")]
public IActionResult ChangePassword()
{
return View();
}
[HttpGet("diagnostic")]
public IActionResult DiagnosticTest()
{
return View();
}
[HttpGet("reset-password-emergency")]
[AllowAnonymous]
public async Task<IActionResult> EmergencyPasswordReset(string confirm, string secret)
{
string text = Environment.GetEnvironmentVariable("EMERGENCY_RESET_SECRET") ?? "skyart-emergency-2025";
if (secret != text)
{
return NotFound();
}
if (confirm != "yes-reset-now")
{
return Content("Add ?confirm=yes-reset-now&secret=YOUR_SECRET to URL to reset admin password");
}
string email = "admin@skyartshop.com";
string newPassword = "Admin123!";
AdminUser adminUser = await _pgService.GetUserByEmailAsync(email);
if (adminUser == null)
{
adminUser = new AdminUser
{
Email = email,
Name = "System Administrator",
Role = "MasterAdmin",
Permissions = new List<string>
{
"manage_users", "manage_products", "manage_orders", "manage_content", "manage_settings", "view_reports", "manage_finances", "manage_inventory", "manage_customers", "manage_blog",
"manage_portfolio", "manage_pages"
},
IsActive = true,
CreatedBy = "Emergency Reset",
CreatedAt = DateTime.UtcNow
};
}
adminUser.PasswordHash = _pgAuthService.HashPassword(newPassword);
adminUser.LastLogin = DateTime.UtcNow;
await _pgService.CreateAdminUserAsync(adminUser);
return Content($"\r\n<!DOCTYPE html>\r\n<html>\r\n<head>\r\n <title>Password Reset Complete</title>\r\n <style>\r\n body {{ font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }}\r\n .success {{ background: #d4edda; border: 1px solid #c3e6cb; padding: 20px; border-radius: 5px; }}\r\n .credentials {{ background: #f8f9fa; padding: 15px; margin: 20px 0; border-left: 4px solid #28a745; }}\r\n a {{ color: #007bff; text-decoration: none; }}\r\n </style>\r\n</head>\r\n<body>\r\n <div class='success'>\r\n <h2>✓ Password Reset Successful</h2>\r\n <p>The admin password has been reset.</p>\r\n <div class='credentials'>\r\n <strong>Login Credentials:</strong><br>\r\n Email: <code>{email}</code><br>\r\n Password: <code>{newPassword}</code>\r\n </div>\r\n <p><a href='/admin/login'>→ Go to Login Page</a></p>\r\n <p><small>For security, this URL will be disabled after first successful login.</small></p>\r\n </div>\r\n</body>\r\n</html>\r\n", "text/html");
}
[HttpPost("change-password")]
public async Task<IActionResult> ChangePassword(string currentPassword, string newPassword, string confirmPassword)
{
if (string.IsNullOrWhiteSpace(currentPassword) || string.IsNullOrWhiteSpace(newPassword))
{
base.ViewBag.Error = "All fields are required";
return View();
}
if (newPassword != confirmPassword)
{
base.ViewBag.Error = "New password and confirmation do not match";
return View();
}
if (newPassword.Length < 6)
{
base.ViewBag.Error = "Password must be at least 6 characters";
return View();
}
string text = base.User.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier")?.Value;
if (string.IsNullOrEmpty(text))
{
base.ViewBag.Error = "User not found";
return View();
}
AdminUser adminUser = await _pgService.GetByIdAsync<AdminUser>("AdminUsers", text);
if (adminUser == null || !_pgAuthService.VerifyPassword(currentPassword, adminUser.PasswordHash))
{
base.ViewBag.Error = "Current password is incorrect";
return View();
}
base.ViewBag.Info = "Password change temporarily disabled during migration. Contact system admin.";
return View();
}
}

View File

@@ -1,225 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
[Route("admin/homepage")]
[Authorize(Roles = "Admin,MasterAdmin")]
public class AdminHomepageController : Controller
{
private readonly PostgreSQLService _pgService;
private readonly IWebHostEnvironment _environment;
private readonly string _sectionsCollection = "HomepageSections";
private readonly string _settingsCollection = "SiteSettings";
public AdminHomepageController(PostgreSQLService pgService, IWebHostEnvironment environment)
{
_pgService = pgService;
_environment = environment;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
List<HomepageSection> sections = (await _pgService.GetAllAsync<HomepageSection>(_sectionsCollection)).OrderBy((HomepageSection s) => s.DisplayOrder).ToList();
SiteSettings siteSettings = (await _pgService.GetAllAsync<SiteSettings>(_settingsCollection)).FirstOrDefault() ?? new SiteSettings();
base.ViewBag.Settings = siteSettings;
return View(sections);
}
[HttpGet("section/{id}")]
public async Task<IActionResult> EditSection(string id)
{
HomepageSection homepageSection = await _pgService.GetByIdAsync<HomepageSection>(_sectionsCollection, id);
if (homepageSection == null)
{
base.TempData["ErrorMessage"] = "Section not found.";
return RedirectToAction("Index");
}
return View(homepageSection);
}
[HttpPost("section/update")]
public async Task<IActionResult> UpdateSection(HomepageSection section, IFormFile? imageFile, string? SelectedImageUrl)
{
base.ModelState.Remove("Content");
base.ModelState.Remove("AdditionalData");
base.ModelState.Remove("SelectedImageUrl");
if (!base.ModelState.IsValid)
{
foreach (ModelError item in base.ModelState.Values.SelectMany((ModelStateEntry v) => v.Errors))
{
Console.WriteLine("ModelState Error: " + item.ErrorMessage);
}
return View("EditSection", section);
}
Console.WriteLine("Updating section with ID: " + section.Id);
Console.WriteLine("Title: " + section.Title);
Console.WriteLine("Subtitle: " + section.Subtitle);
Console.WriteLine($"Content length: {section.Content?.Length ?? 0}");
Console.WriteLine($"IsActive: {section.IsActive}");
HomepageSection existingSection = await _pgService.GetByIdAsync<HomepageSection>(_sectionsCollection, section.Id);
if (existingSection == null)
{
Console.WriteLine("ERROR: Section with ID " + section.Id + " not found!");
base.TempData["ErrorMessage"] = "Section not found.";
return RedirectToAction("Index");
}
Console.WriteLine("Found existing section: " + existingSection.Title);
existingSection.SectionType = section.SectionType;
existingSection.Title = section.Title ?? string.Empty;
existingSection.Subtitle = section.Subtitle ?? string.Empty;
existingSection.Content = section.Content ?? string.Empty;
existingSection.ButtonText = section.ButtonText ?? string.Empty;
existingSection.ButtonUrl = section.ButtonUrl ?? string.Empty;
existingSection.IsActive = section.IsActive;
existingSection.UpdatedAt = DateTime.UtcNow;
if (existingSection.AdditionalData == null)
{
existingSection.AdditionalData = new Dictionary<string, string>();
}
if (!string.IsNullOrEmpty(SelectedImageUrl))
{
existingSection.ImageUrl = SelectedImageUrl;
Console.WriteLine("Image selected from library: " + existingSection.ImageUrl);
}
else if (imageFile != null && imageFile.Length > 0)
{
string text = Path.Combine(_environment.WebRootPath, "uploads", "images");
Directory.CreateDirectory(text);
string uniqueFileName = $"{Guid.NewGuid()}_{imageFile.FileName}";
string path = Path.Combine(text, uniqueFileName);
using (FileStream fileStream = new FileStream(path, FileMode.Create))
{
await imageFile.CopyToAsync(fileStream);
}
existingSection.ImageUrl = "/uploads/images/" + uniqueFileName;
Console.WriteLine("New image uploaded: " + existingSection.ImageUrl);
}
await _pgService.UpdateAsync(_sectionsCollection, existingSection.Id, existingSection);
Console.WriteLine("Section updated successfully in database");
base.TempData["SuccessMessage"] = "Section updated successfully!";
return RedirectToAction("Index");
}
[HttpGet("section/create")]
public IActionResult CreateSection()
{
return View();
}
[HttpPost("section/create")]
public async Task<IActionResult> CreateSection(HomepageSection section, IFormFile? imageFile, string? SelectedImageUrl)
{
base.ModelState.Remove("Content");
base.ModelState.Remove("AdditionalData");
if (!base.ModelState.IsValid)
{
return View(section);
}
if (!string.IsNullOrEmpty(SelectedImageUrl))
{
section.ImageUrl = SelectedImageUrl;
Console.WriteLine("Image selected from library: " + section.ImageUrl);
}
else if (imageFile != null && imageFile.Length > 0)
{
string text = Path.Combine(_environment.WebRootPath, "uploads", "images");
Directory.CreateDirectory(text);
string uniqueFileName = $"{Guid.NewGuid()}_{imageFile.FileName}";
string path = Path.Combine(text, uniqueFileName);
using (FileStream fileStream = new FileStream(path, FileMode.Create))
{
await imageFile.CopyToAsync(fileStream);
}
section.ImageUrl = "/uploads/images/" + uniqueFileName;
}
if (section.AdditionalData == null)
{
section.AdditionalData = new Dictionary<string, string>();
}
List<HomepageSection> source = await _pgService.GetAllAsync<HomepageSection>(_sectionsCollection);
section.DisplayOrder = (source.Any() ? (source.Max((HomepageSection s) => s.DisplayOrder) + 1) : 0);
section.CreatedAt = DateTime.UtcNow;
section.UpdatedAt = DateTime.UtcNow;
await _pgService.InsertAsync(_sectionsCollection, section);
base.TempData["SuccessMessage"] = "Section created successfully!";
return RedirectToAction("Index");
}
[HttpPost("section/delete/{id}")]
public async Task<IActionResult> DeleteSection(string id)
{
await _pgService.DeleteAsync<HomepageSection>(_sectionsCollection, id);
base.TempData["SuccessMessage"] = "Section deleted successfully!";
return RedirectToAction("Index");
}
[HttpPost("section/reorder")]
public async Task<IActionResult> ReorderSections([FromBody] List<string> sectionIds)
{
for (int i = 0; i < sectionIds.Count; i++)
{
HomepageSection homepageSection = await _pgService.GetByIdAsync<HomepageSection>(_sectionsCollection, sectionIds[i]);
if (homepageSection != null)
{
homepageSection.DisplayOrder = i;
homepageSection.UpdatedAt = DateTime.UtcNow;
await _pgService.UpdateAsync(_sectionsCollection, homepageSection.Id, homepageSection);
}
}
return Json(new
{
success = true
});
}
[HttpPost("section/toggle/{id}")]
public async Task<IActionResult> ToggleSection(string id)
{
HomepageSection section = await _pgService.GetByIdAsync<HomepageSection>(_sectionsCollection, id);
if (section != null)
{
section.IsActive = !section.IsActive;
section.UpdatedAt = DateTime.UtcNow;
await _pgService.UpdateAsync(_sectionsCollection, section.Id, section);
base.TempData["SuccessMessage"] = "Section " + (section.IsActive ? "activated" : "deactivated") + " successfully!";
}
return RedirectToAction("Index");
}
[HttpPost("footer/update")]
public async Task<IActionResult> UpdateFooter(string footerText)
{
SiteSettings siteSettings = (await _pgService.GetAllAsync<SiteSettings>(_settingsCollection)).FirstOrDefault();
if (siteSettings == null)
{
siteSettings = new SiteSettings
{
FooterText = footerText
};
await _pgService.InsertAsync(_settingsCollection, siteSettings);
}
else
{
siteSettings.FooterText = footerText;
siteSettings.UpdatedAt = DateTime.UtcNow;
await _pgService.UpdateAsync(_settingsCollection, siteSettings.Id, siteSettings);
}
base.TempData["SuccessMessage"] = "Footer updated successfully!";
return RedirectToAction("Index");
}
}

View File

@@ -1,187 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
[Route("admin/menu")]
[Authorize(Roles = "Admin,MasterAdmin")]
public class AdminMenuController : Controller
{
private readonly PostgreSQLService _pgService;
public AdminMenuController(PostgreSQLService pgService)
{
_pgService = pgService;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
return View((await _pgService.GetAllAsync<MenuItem>("MenuItems")).OrderBy((MenuItem m) => m.DisplayOrder).ToList());
}
[HttpPost("reseed")]
public async Task<IActionResult> ReseedMenu()
{
foreach (MenuItem item in await _pgService.GetAllAsync<MenuItem>("MenuItems"))
{
await _pgService.DeleteAsync<MenuItem>("MenuItems", item.Id);
}
MenuItem[] array = new MenuItem[10]
{
new MenuItem
{
Label = "Home",
Url = "/",
DisplayOrder = 1,
IsActive = true,
ShowInNavbar = true,
ShowInDropdown = true
},
new MenuItem
{
Label = "Shop",
Url = "/Shop",
DisplayOrder = 2,
IsActive = true,
ShowInNavbar = true,
ShowInDropdown = true
},
new MenuItem
{
Label = "Top Sellers",
Url = "/#top-sellers",
DisplayOrder = 3,
IsActive = true,
ShowInNavbar = true,
ShowInDropdown = true
},
new MenuItem
{
Label = "Promotion",
Url = "/#promotion",
DisplayOrder = 4,
IsActive = true,
ShowInNavbar = true,
ShowInDropdown = true
},
new MenuItem
{
Label = "Portfolio",
Url = "/Portfolio",
DisplayOrder = 5,
IsActive = true,
ShowInNavbar = true,
ShowInDropdown = true
},
new MenuItem
{
Label = "Blog",
Url = "/Blog",
DisplayOrder = 6,
IsActive = true,
ShowInNavbar = true,
ShowInDropdown = true
},
new MenuItem
{
Label = "About",
Url = "/About",
DisplayOrder = 7,
IsActive = true,
ShowInNavbar = true,
ShowInDropdown = true
},
new MenuItem
{
Label = "Instagram",
Url = "#instagram",
DisplayOrder = 8,
IsActive = true,
ShowInNavbar = false,
ShowInDropdown = true
},
new MenuItem
{
Label = "Contact",
Url = "/Contact",
DisplayOrder = 9,
IsActive = true,
ShowInNavbar = true,
ShowInDropdown = true
},
new MenuItem
{
Label = "My Wishlist",
Url = "#wishlist",
DisplayOrder = 10,
IsActive = true,
ShowInNavbar = false,
ShowInDropdown = true
}
};
MenuItem[] array2 = array;
foreach (MenuItem entity in array2)
{
await _pgService.InsertAsync("MenuItems", entity);
}
base.TempData["SuccessMessage"] = "Menu items reseeded successfully!";
return RedirectToAction("Index");
}
[HttpGet("create")]
public IActionResult Create()
{
return View(new MenuItem());
}
[HttpPost("create")]
public async Task<IActionResult> Create(MenuItem menuItem)
{
if (!base.ModelState.IsValid)
{
return View(menuItem);
}
menuItem.CreatedAt = DateTime.UtcNow;
await _pgService.InsertAsync("MenuItems", menuItem);
base.TempData["SuccessMessage"] = "Menu item created successfully!";
return RedirectToAction("Index");
}
[HttpGet("edit/{id}")]
public async Task<IActionResult> Edit(string id)
{
MenuItem menuItem = await _pgService.GetByIdAsync<MenuItem>("MenuItems", id);
if (menuItem == null)
{
return NotFound();
}
return View(menuItem);
}
[HttpPost("edit/{id}")]
public async Task<IActionResult> Edit(string id, MenuItem menuItem)
{
if (!base.ModelState.IsValid)
{
return View(menuItem);
}
menuItem.Id = id;
await _pgService.UpdateAsync("MenuItems", id, menuItem);
base.TempData["SuccessMessage"] = "Menu item updated successfully!";
return RedirectToAction("Index");
}
[HttpPost("delete/{id}")]
public async Task<IActionResult> Delete(string id)
{
await _pgService.DeleteAsync<MenuItem>("MenuItems", id);
base.TempData["SuccessMessage"] = "Menu item deleted successfully!";
return RedirectToAction("Index");
}
}

View File

@@ -1,163 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
[Route("admin/pages")]
[Authorize(Roles = "Admin,MasterAdmin")]
public class AdminPagesController : Controller
{
private readonly PostgreSQLService _pgService;
private readonly SlugService _slugService;
private readonly string _pagesCollection = "Pages";
public AdminPagesController(PostgreSQLService pgService, SlugService slugService)
{
_pgService = pgService;
_slugService = slugService;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
return View((await _pgService.GetAllAsync<Page>(_pagesCollection)).OrderBy((Page p) => p.PageName).ToList());
}
[HttpGet("create")]
public IActionResult Create()
{
return View(new Page());
}
[HttpPost("create")]
public async Task<IActionResult> Create(Page page)
{
if (!base.ModelState.IsValid)
{
return View(page);
}
page.CreatedAt = DateTime.UtcNow;
page.UpdatedAt = DateTime.UtcNow;
page.PageSlug = _slugService.GenerateSlug(page.PageName);
await _pgService.InsertAsync(_pagesCollection, page);
base.TempData["SuccessMessage"] = "Page created successfully!";
return RedirectToAction("Index");
}
[HttpGet("edit/{id}")]
public async Task<IActionResult> Edit(string id)
{
Page page = await _pgService.GetByIdAsync<Page>(_pagesCollection, id);
if (page == null)
{
return NotFound();
}
return View(page);
}
[HttpPost("edit/{id}")]
public async Task<IActionResult> Edit(string id, [FromForm] Page page, IFormCollection form)
{
Console.WriteLine("[ADMIN-PAGES] === FORM SUBMISSION DEBUG ===");
Console.WriteLine("[ADMIN-PAGES] Form Keys: " + string.Join(", ", form.Keys));
foreach (string key in form.Keys)
{
if (key.StartsWith("ImageGallery") || key.StartsWith("TeamMembers"))
{
Console.WriteLine($"[ADMIN-PAGES] {key} = {form[key]}");
}
}
if (!base.ModelState.IsValid)
{
Console.WriteLine("[ADMIN-PAGES] ModelState is INVALID");
foreach (ModelError item2 in base.ModelState.Values.SelectMany((ModelStateEntry v) => v.Errors))
{
Console.WriteLine("[ADMIN-PAGES] Error: " + item2.ErrorMessage);
}
return View(page);
}
Page page2 = await _pgService.GetByIdAsync<Page>(_pagesCollection, id);
if (page2 == null)
{
Console.WriteLine("[ADMIN-PAGES] Page not found: " + id);
return NotFound();
}
page2.PageName = page.PageName;
page2.Title = page.Title;
page2.Subtitle = page.Subtitle;
page2.Content = page.Content;
page2.IsActive = page.IsActive;
page2.UpdatedAt = DateTime.UtcNow;
page2.PageSlug = _slugService.GenerateSlug(page.PageName);
page2.AboutImage1 = form["AboutImage1"].ToString() ?? string.Empty;
page2.AboutImage2 = form["AboutImage2"].ToString() ?? string.Empty;
page2.ImageGallery = new List<string>();
foreach (string item3 in form.Keys.Where((string k) => k.StartsWith("ImageGallery[")))
{
string text = form[item3].ToString();
if (!string.IsNullOrEmpty(text))
{
page2.ImageGallery.Add(text);
}
}
page2.TeamMembers = new List<TeamMember>();
List<int> list = (from i in (from i in form.Keys.Where((string k) => k.StartsWith("TeamMembers[") && k.Contains("].Name")).Select(delegate(string k)
{
Match match = Regex.Match(k, "TeamMembers\\[(\\d+)\\]");
return (!match.Success) ? (-1) : int.Parse(match.Groups[1].Value);
})
where i >= 0
select i).Distinct()
orderby i
select i).ToList();
foreach (int item4 in list)
{
TeamMember item = new TeamMember
{
Name = form[$"TeamMembers[{item4}].Name"].ToString(),
Role = form[$"TeamMembers[{item4}].Role"].ToString(),
Bio = form[$"TeamMembers[{item4}].Bio"].ToString(),
PhotoUrl = form[$"TeamMembers[{item4}].PhotoUrl"].ToString()
};
page2.TeamMembers.Add(item);
}
Console.WriteLine($"[ADMIN-PAGES] Updating page: {page2.PageName} (Slug: {page2.PageSlug})");
Console.WriteLine("[ADMIN-PAGES] Title: " + page2.Title);
Console.WriteLine($"[ADMIN-PAGES] Content length: {page2.Content?.Length ?? 0}");
Console.WriteLine($"[ADMIN-PAGES] Image Gallery Count: {page2.ImageGallery.Count}");
Console.WriteLine($"[ADMIN-PAGES] Team Members Count: {page2.TeamMembers.Count}");
if (page2.ImageGallery.Any())
{
Console.WriteLine("[ADMIN-PAGES] Gallery Images: " + string.Join(", ", page2.ImageGallery));
}
if (page2.TeamMembers.Any())
{
foreach (TeamMember teamMember in page2.TeamMembers)
{
Console.WriteLine($"[ADMIN-PAGES] Team Member: {teamMember.Name} - {teamMember.Role} - Photo: {teamMember.PhotoUrl}");
}
}
await _pgService.UpdateAsync(_pagesCollection, id, page2);
base.TempData["SuccessMessage"] = "Page updated successfully!";
return RedirectToAction("Index");
}
[HttpPost("delete/{id}")]
public async Task<IActionResult> Delete(string id)
{
await _pgService.DeleteAsync<Page>(_pagesCollection, id);
base.TempData["SuccessMessage"] = "Page deleted successfully!";
return RedirectToAction("Index");
}
}

View File

@@ -1,165 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
[Route("admin/portfolio")]
[Authorize(Roles = "Admin,MasterAdmin")]
public class AdminPortfolioController : Controller
{
private readonly PostgreSQLService _pgService;
private readonly SlugService _slugService;
private readonly string _categoriesCollection = "PortfolioCategories";
private readonly string _projectsCollection = "PortfolioProjects";
public AdminPortfolioController(PostgreSQLService pgService, SlugService slugService)
{
_pgService = pgService;
_slugService = slugService;
}
[HttpGet("categories")]
public async Task<IActionResult> Categories()
{
return View((await _pgService.GetAllAsync<PortfolioCategory>(_categoriesCollection)).OrderBy((PortfolioCategory c) => c.DisplayOrder).ToList());
}
[HttpGet("category/create")]
public IActionResult CreateCategory()
{
return View(new PortfolioCategory());
}
[HttpPost("category/create")]
public async Task<IActionResult> CreateCategory(PortfolioCategory category)
{
if (!base.ModelState.IsValid)
{
return View(category);
}
category.CreatedAt = DateTime.UtcNow;
category.UpdatedAt = DateTime.UtcNow;
category.Slug = _slugService.GenerateSlug(category.Name);
await _pgService.InsertAsync(_categoriesCollection, category);
base.TempData["SuccessMessage"] = "Category created successfully!";
return RedirectToAction("Categories");
}
[HttpGet("category/edit/{id}")]
public async Task<IActionResult> EditCategory(string id)
{
PortfolioCategory portfolioCategory = await _pgService.GetByIdAsync<PortfolioCategory>(_categoriesCollection, id);
if (portfolioCategory == null)
{
return NotFound();
}
return View(portfolioCategory);
}
[HttpPost("category/edit/{id}")]
public async Task<IActionResult> EditCategory(string id, PortfolioCategory category)
{
if (!base.ModelState.IsValid)
{
return View(category);
}
category.Id = id;
category.UpdatedAt = DateTime.UtcNow;
category.Slug = _slugService.GenerateSlug(category.Name);
await _pgService.UpdateAsync(_categoriesCollection, id, category);
base.TempData["SuccessMessage"] = "Category updated successfully!";
return RedirectToAction("Categories");
}
[HttpPost("category/delete/{id}")]
public async Task<IActionResult> DeleteCategory(string id)
{
await _pgService.DeleteAsync<PortfolioCategory>(_categoriesCollection, id);
base.TempData["SuccessMessage"] = "Category deleted successfully!";
return RedirectToAction("Categories");
}
[HttpGet("projects")]
public async Task<IActionResult> Projects(string? categoryId)
{
List<PortfolioProject> projects = await _pgService.GetAllAsync<PortfolioProject>(_projectsCollection);
List<PortfolioCategory> source = await _pgService.GetAllAsync<PortfolioCategory>(_categoriesCollection);
if (!string.IsNullOrEmpty(categoryId))
{
projects = projects.Where((PortfolioProject p) => p.CategoryId == categoryId).ToList();
}
base.ViewBag.Categories = source.Where((PortfolioCategory c) => c.IsActive).ToList();
base.ViewBag.SelectedCategory = categoryId;
return View(projects.OrderBy((PortfolioProject p) => p.DisplayOrder).ToList());
}
[HttpGet("project/create")]
public async Task<IActionResult> CreateProject()
{
List<PortfolioCategory> source = await _pgService.GetAllAsync<PortfolioCategory>(_categoriesCollection);
base.ViewBag.Categories = source.Where((PortfolioCategory c) => c.IsActive).ToList();
return View(new PortfolioProject());
}
[HttpPost("project/create")]
public async Task<IActionResult> CreateProject(PortfolioProject project)
{
if (!base.ModelState.IsValid)
{
List<PortfolioCategory> source = await _pgService.GetAllAsync<PortfolioCategory>(_categoriesCollection);
base.ViewBag.Categories = source.Where((PortfolioCategory c) => c.IsActive).ToList();
return View(project);
}
project.CreatedAt = DateTime.UtcNow;
project.UpdatedAt = DateTime.UtcNow;
await _pgService.InsertAsync(_projectsCollection, project);
base.TempData["SuccessMessage"] = "Project created successfully!";
return RedirectToAction("Projects");
}
[HttpGet("project/edit/{id}")]
public async Task<IActionResult> EditProject(string id)
{
PortfolioProject project = await _pgService.GetByIdAsync<PortfolioProject>(_projectsCollection, id);
if (project == null)
{
return NotFound();
}
List<PortfolioCategory> source = await _pgService.GetAllAsync<PortfolioCategory>(_categoriesCollection);
base.ViewBag.Categories = source.Where((PortfolioCategory c) => c.IsActive).ToList();
return View(project);
}
[HttpPost("project/edit/{id}")]
public async Task<IActionResult> EditProject(string id, PortfolioProject project)
{
if (!base.ModelState.IsValid)
{
List<PortfolioCategory> source = await _pgService.GetAllAsync<PortfolioCategory>(_categoriesCollection);
base.ViewBag.Categories = source.Where((PortfolioCategory c) => c.IsActive).ToList();
return View(project);
}
project.Id = id;
project.UpdatedAt = DateTime.UtcNow;
await _pgService.UpdateAsync(_projectsCollection, id, project);
base.TempData["SuccessMessage"] = "Project updated successfully!";
return RedirectToAction("Projects");
}
[HttpPost("project/delete/{id}")]
public async Task<IActionResult> DeleteProject(string id)
{
await _pgService.DeleteAsync<PortfolioProject>(_projectsCollection, id);
base.TempData["SuccessMessage"] = "Project deleted successfully!";
return RedirectToAction("Projects");
}
}

View File

@@ -1,265 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
[Route("admin/products")]
[Authorize(Roles = "Admin,MasterAdmin")]
public class AdminProductsController : Controller
{
private readonly PostgreSQLService _pgService;
private readonly SlugService _slugService;
private readonly string _productsCollection = "Products";
public AdminProductsController(PostgreSQLService pgService, SlugService slugService)
{
_pgService = pgService;
_slugService = slugService;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
return View((await _pgService.GetAllAsync<Product>(_productsCollection)).OrderByDescending((Product p) => p.CreatedAt).ToList());
}
[HttpGet("create")]
public IActionResult Create()
{
return View(new Product());
}
[HttpPost("create")]
public async Task<IActionResult> Create(Product product, string? ProductVariantsJson)
{
try
{
base.ModelState.Remove("ShortDescription");
base.ModelState.Remove("Description");
base.ModelState.Remove("ProductVariantsJson");
base.ModelState.Remove("Id");
base.ModelState.Remove("Slug");
base.ModelState.Remove("ImageUrl");
base.ModelState.Remove("Images");
if (!base.ModelState.IsValid)
{
foreach (ModelError item in base.ModelState.Values.SelectMany((ModelStateEntry v) => v.Errors))
{
Console.WriteLine("[CREATE] Validation Error: " + item.ErrorMessage);
}
return View(product);
}
}
catch (Exception ex)
{
Console.WriteLine("[CREATE] Error: " + ex.Message);
Console.WriteLine("[CREATE] Stack: " + ex.StackTrace);
base.TempData["ErrorMessage"] = "Error creating product: " + ex.Message;
return View(product);
}
if (!base.Request.Form.ContainsKey("IsActive"))
{
product.IsActive = false;
}
if (!base.Request.Form.ContainsKey("IsFeatured"))
{
product.IsFeatured = false;
}
if (!base.Request.Form.ContainsKey("IsTopSeller"))
{
product.IsTopSeller = false;
}
Console.WriteLine("[CREATE] ProductVariantsJson received: '" + (ProductVariantsJson ?? "NULL") + "'");
if (!string.IsNullOrEmpty(ProductVariantsJson))
{
try
{
product.Variants = JsonSerializer.Deserialize<List<ProductVariant>>(ProductVariantsJson) ?? new List<ProductVariant>();
Console.WriteLine($"[CREATE] Variants deserialized successfully: {product.Variants.Count} variants");
foreach (ProductVariant variant in product.Variants)
{
Console.WriteLine($" - {variant.ColorName} ({variant.ColorHex}) with {variant.Images?.Count ?? 0} images, Stock: {variant.StockQuantity}");
}
}
catch (Exception ex2)
{
Console.WriteLine("[CREATE] Error parsing variants: " + ex2.Message);
Console.WriteLine("[CREATE] JSON was: " + ProductVariantsJson);
product.Variants = new List<ProductVariant>();
}
}
else
{
Console.WriteLine("[CREATE] No variants provided - ProductVariantsJson is null or empty");
product.Variants = new List<ProductVariant>();
}
product.Colors = new List<string>();
product.Color = string.Empty;
try
{
product.CreatedAt = DateTime.UtcNow;
product.UpdatedAt = DateTime.UtcNow;
product.Slug = _slugService.GenerateSlug(product.Name);
await _pgService.InsertAsync(_productsCollection, product);
base.TempData["SuccessMessage"] = "Product created successfully!";
return RedirectToAction("Index");
}
catch (Exception ex3)
{
Console.WriteLine("[CREATE] Database Error: " + ex3.Message);
Console.WriteLine("[CREATE] Stack: " + ex3.StackTrace);
base.TempData["ErrorMessage"] = "Error saving product: " + ex3.Message;
return View(product);
}
}
[HttpGet("edit/{id}")]
public async Task<IActionResult> Edit(string id)
{
Product product = await _pgService.GetByIdAsync<Product>(_productsCollection, id);
if (product == null)
{
return NotFound();
}
return View("Create", product);
}
[HttpPost("edit/{id}")]
public async Task<IActionResult> Edit(string id, Product product, string? ProductVariantsJson)
{
try
{
base.ModelState.Remove("Images");
base.ModelState.Remove("Slug");
base.ModelState.Remove("ShortDescription");
base.ModelState.Remove("Description");
base.ModelState.Remove("ProductVariantsJson");
base.ModelState.Remove("Id");
base.ModelState.Remove("ImageUrl");
if (!base.ModelState.IsValid)
{
foreach (ModelError item in base.ModelState.Values.SelectMany((ModelStateEntry v) => v.Errors))
{
Console.WriteLine("[EDIT] Validation Error: " + item.ErrorMessage);
}
return View("Create", product);
}
}
catch (Exception ex)
{
Console.WriteLine("[EDIT] Error: " + ex.Message);
Console.WriteLine("[EDIT] Stack: " + ex.StackTrace);
base.TempData["ErrorMessage"] = "Error updating product: " + ex.Message;
return View("Create", product);
}
if (!base.Request.Form.ContainsKey("IsActive"))
{
product.IsActive = false;
}
if (!base.Request.Form.ContainsKey("IsFeatured"))
{
product.IsFeatured = false;
}
if (!base.Request.Form.ContainsKey("IsTopSeller"))
{
product.IsTopSeller = false;
}
Product existingProduct = await _pgService.GetByIdAsync<Product>(_productsCollection, id);
if (existingProduct == null)
{
base.TempData["ErrorMessage"] = "Product not found.";
return RedirectToAction("Index");
}
existingProduct.Name = product.Name;
existingProduct.ShortDescription = product.ShortDescription;
existingProduct.Description = product.Description;
existingProduct.Price = product.Price;
existingProduct.Category = product.Category;
existingProduct.Color = product.Color;
Console.WriteLine("[EDIT] ProductVariantsJson received: '" + (ProductVariantsJson ?? "NULL") + "'");
if (!string.IsNullOrEmpty(ProductVariantsJson))
{
try
{
existingProduct.Variants = JsonSerializer.Deserialize<List<ProductVariant>>(ProductVariantsJson) ?? new List<ProductVariant>();
Console.WriteLine($"[EDIT] Variants deserialized successfully: {existingProduct.Variants.Count} variants");
foreach (ProductVariant variant in existingProduct.Variants)
{
Console.WriteLine($" - {variant.ColorName} ({variant.ColorHex}) with {variant.Images?.Count ?? 0} images, Stock: {variant.StockQuantity}");
}
}
catch (Exception ex2)
{
Console.WriteLine("[EDIT] Error parsing variants: " + ex2.Message);
Console.WriteLine("[EDIT] JSON was: " + ProductVariantsJson);
existingProduct.Variants = new List<ProductVariant>();
}
}
else
{
Console.WriteLine("[EDIT] No variants provided - ProductVariantsJson is null or empty");
existingProduct.Variants = new List<ProductVariant>();
}
existingProduct.Colors = new List<string>();
existingProduct.Color = string.Empty;
existingProduct.StockQuantity = product.StockQuantity;
existingProduct.IsFeatured = product.IsFeatured;
existingProduct.IsTopSeller = product.IsTopSeller;
existingProduct.IsActive = product.IsActive;
existingProduct.UpdatedAt = DateTime.UtcNow;
existingProduct.Slug = _slugService.GenerateSlug(product.Name);
if (base.Request.Form.ContainsKey("Images"))
{
List<string> list = (existingProduct.Images = (from img in base.Request.Form["Images"]
where !string.IsNullOrEmpty(img)
select (img)).ToList());
if (list.Any() && string.IsNullOrEmpty(product.ImageUrl))
{
existingProduct.ImageUrl = list[0] ?? "";
}
}
if (!string.IsNullOrEmpty(product.ImageUrl))
{
existingProduct.ImageUrl = product.ImageUrl;
}
if (!string.IsNullOrEmpty(product.SKU))
{
existingProduct.SKU = product.SKU;
}
if (product.CostPrice > 0m)
{
existingProduct.CostPrice = product.CostPrice;
}
try
{
await _pgService.UpdateAsync(_productsCollection, id, existingProduct);
base.TempData["SuccessMessage"] = "Product updated successfully!";
return RedirectToAction("Index");
}
catch (Exception ex3)
{
Console.WriteLine("[EDIT] Database Error: " + ex3.Message);
Console.WriteLine("[EDIT] Stack: " + ex3.StackTrace);
base.TempData["ErrorMessage"] = "Error saving product changes: " + ex3.Message;
return View("Create", existingProduct);
}
}
[HttpPost("delete/{id}")]
public async Task<IActionResult> Delete(string id)
{
await _pgService.DeleteAsync<Product>(_productsCollection, id);
base.TempData["SuccessMessage"] = "Product deleted successfully!";
return RedirectToAction("Index");
}
}

View File

@@ -1,55 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
[Route("admin/settings")]
[Authorize(Roles = "Admin,MasterAdmin")]
public class AdminSettingsController : Controller
{
private readonly PostgreSQLService _pgService;
private readonly string _settingsCollection = "SiteSettings";
public AdminSettingsController(PostgreSQLService pgService)
{
_pgService = pgService;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
SiteSettings settings = (await _pgService.GetAllAsync<SiteSettings>(_settingsCollection)).FirstOrDefault();
if (settings == null)
{
settings = new SiteSettings();
await _pgService.InsertAsync(_settingsCollection, settings);
}
return View(settings);
}
[HttpPost("update")]
public async Task<IActionResult> Update(SiteSettings settings)
{
if (!base.ModelState.IsValid)
{
return View("Index", settings);
}
settings.UpdatedAt = DateTime.UtcNow;
if (string.IsNullOrEmpty(settings.Id))
{
await _pgService.InsertAsync(_settingsCollection, settings);
}
else
{
await _pgService.UpdateAsync(_settingsCollection, settings.Id, settings);
}
base.TempData["SuccessMessage"] = "Site settings updated successfully!";
return RedirectToAction("Index");
}
}

View File

@@ -1,268 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace SkyArtShop.Controllers;
[Route("admin/upload")]
[Authorize(Roles = "Admin,MasterAdmin")]
[IgnoreAntiforgeryToken]
public class AdminUploadController : Controller
{
private readonly IWebHostEnvironment _environment;
public AdminUploadController(IWebHostEnvironment environment)
{
_environment = environment;
}
[HttpGet("")]
public IActionResult Index()
{
string path = Path.Combine(_environment.WebRootPath, "uploads", "images");
List<string> model = new List<string>();
if (Directory.Exists(path))
{
List<string> list = (from f in Directory.GetFiles(path)
select "/uploads/images/" + Path.GetFileName(f) into f
orderby f descending
select f).ToList();
model = list;
}
return View(model);
}
[HttpPost("image")]
public async Task<IActionResult> UploadImage(IFormFile file)
{
if (file == null || file.Length == 0L)
{
return Json(new
{
success = false,
message = "No file uploaded"
});
}
string[] source = new string[5] { ".jpg", ".jpeg", ".png", ".gif", ".webp" };
string value = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!source.Contains(value))
{
return Json(new
{
success = false,
message = "Invalid file type"
});
}
try
{
string text = Path.Combine(_environment.WebRootPath, "uploads", "images");
if (!Directory.Exists(text))
{
Directory.CreateDirectory(text);
}
string fileName = $"{Guid.NewGuid()}{value}";
string path = Path.Combine(text, fileName);
using FileStream stream = new FileStream(path, FileMode.Create);
await file.CopyToAsync(stream);
return Json(new
{
success = true,
url = "/uploads/images/" + fileName
});
}
catch (Exception ex)
{
return Json(new
{
success = false,
message = ex.Message
});
}
}
[HttpPost("multiple")]
public async Task<IActionResult> UploadMultiple(List<IFormFile> files)
{
List<string> uploadedUrls = new List<string>();
foreach (IFormFile file in files)
{
if (file == null || file.Length == 0L)
{
continue;
}
string value = Path.GetExtension(file.FileName).ToLowerInvariant();
string[] source = new string[5] { ".jpg", ".jpeg", ".png", ".gif", ".webp" };
if (source.Contains(value))
{
string text = Path.Combine(_environment.WebRootPath, "uploads", "images");
if (!Directory.Exists(text))
{
Directory.CreateDirectory(text);
}
string fileName = $"{Guid.NewGuid()}{value}";
string path = Path.Combine(text, fileName);
using FileStream stream = new FileStream(path, FileMode.Create);
await file.CopyToAsync(stream);
uploadedUrls.Add("/uploads/images/" + fileName);
}
}
return Json(new
{
success = true,
urls = uploadedUrls
});
}
[HttpPost("delete")]
public IActionResult DeleteImage([FromBody] string imageUrl)
{
try
{
string fileName = Path.GetFileName(imageUrl);
string path = Path.Combine(_environment.WebRootPath, "uploads", "images", fileName);
if (System.IO.File.Exists(path))
{
System.IO.File.Delete(path);
return Json(new
{
success = true
});
}
return Json(new
{
success = false,
message = "File not found"
});
}
catch (Exception ex)
{
return Json(new
{
success = false,
message = ex.Message
});
}
}
[HttpGet("list")]
public IActionResult ListImages()
{
string uploadsPath = Path.Combine(_environment.WebRootPath, "uploads", "images");
List<string> data = new List<string>();
if (Directory.Exists(uploadsPath))
{
List<string> list = (from f in Directory.GetFiles(uploadsPath)
select "/uploads/images/" + Path.GetFileName(f) into f
orderby System.IO.File.GetCreationTime(Path.Combine(uploadsPath, Path.GetFileName(f))) descending
select f).ToList();
data = list;
}
return Json(data);
}
[HttpPost("create-folder")]
public IActionResult CreateFolder([FromBody] string folderName)
{
try
{
if (string.IsNullOrWhiteSpace(folderName))
{
return Json(new
{
success = false,
message = "Folder name cannot be empty"
});
}
string text = string.Join("_", folderName.Split(Path.GetInvalidFileNameChars()));
string path = Path.Combine(_environment.WebRootPath, "uploads", "images", text);
if (Directory.Exists(path))
{
return Json(new
{
success = false,
message = "Folder already exists"
});
}
Directory.CreateDirectory(path);
return Json(new
{
success = true,
folderName = text
});
}
catch (Exception ex)
{
return Json(new
{
success = false,
message = ex.Message
});
}
}
[HttpPost("delete-folder")]
public IActionResult DeleteFolder([FromBody] string folderPath)
{
try
{
string path = Path.Combine(_environment.WebRootPath, "uploads", "images", folderPath);
if (!Directory.Exists(path))
{
return Json(new
{
success = false,
message = "Folder not found"
});
}
Directory.Delete(path, recursive: true);
return Json(new
{
success = true
});
}
catch (Exception ex)
{
return Json(new
{
success = false,
message = ex.Message
});
}
}
[HttpGet("list-folders")]
public IActionResult ListFolders()
{
try
{
string path = Path.Combine(_environment.WebRootPath, "uploads", "images");
List<object> data = new List<object>();
if (Directory.Exists(path))
{
var source = (from d in Directory.GetDirectories(path)
select new
{
name = Path.GetFileName(d),
path = Path.GetFileName(d),
fileCount = Directory.GetFiles(d).Length
}).ToList();
data = source.Cast<object>().ToList();
}
return Json(data);
}
catch (Exception ex)
{
return Json(new
{
success = false,
message = ex.Message
});
}
}
}

View File

@@ -1,160 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
[Authorize(Roles = "Admin,MasterAdmin")]
[Route("admin/users")]
public class AdminUsersController : Controller
{
private readonly PostgreSQLService _pgService;
private readonly PostgreAuthService _authService;
public AdminUsersController(PostgreSQLService pgService, PostgreAuthService authService)
{
_pgService = pgService;
_authService = authService;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
return View((await _pgService.GetAllAsync<AdminUser>("AdminUsers")).OrderBy((AdminUser u) => u.CreatedAt).ToList());
}
[HttpGet("create")]
public IActionResult Create()
{
base.ViewBag.Roles = GetAvailableRoles();
return View();
}
[HttpPost("create")]
public async Task<IActionResult> Create(AdminUser user, string password)
{
if (string.IsNullOrWhiteSpace(password))
{
base.ModelState.AddModelError("", "Password is required");
base.ViewBag.Roles = GetAvailableRoles();
return View(user);
}
if (await _authService.GetUserByEmailAsync(user.Email) != null)
{
base.ModelState.AddModelError("", "Email already exists");
base.ViewBag.Roles = GetAvailableRoles();
return View(user);
}
AdminUser adminUser = await _authService.CreateUserAsync(user.Email, password, user.Name, user.Role);
adminUser.Phone = user.Phone;
adminUser.Notes = user.Notes;
adminUser.Permissions = GetRolePermissions(user.Role);
adminUser.CreatedBy = base.User.Identity?.Name ?? "System";
adminUser.PasswordNeverExpires = user.PasswordNeverExpires;
adminUser.PasswordExpiresAt = (user.PasswordNeverExpires ? ((DateTime?)null) : new DateTime?(DateTime.UtcNow.AddDays(90.0)));
await _pgService.UpdateAsync("AdminUsers", adminUser.Id, adminUser);
base.TempData["Success"] = "User " + user.Name + " created successfully! They can now login.";
return RedirectToAction("Index");
}
[HttpGet("edit/{id}")]
public async Task<IActionResult> Edit(string id)
{
AdminUser adminUser = await _pgService.GetByIdAsync<AdminUser>("AdminUsers", id);
if (adminUser == null)
{
return NotFound();
}
base.ViewBag.Roles = GetAvailableRoles();
return View(adminUser);
}
[HttpPost("edit/{id}")]
public async Task<IActionResult> Edit(string id, AdminUser user, string? newPassword)
{
AdminUser adminUser = await _pgService.GetByIdAsync<AdminUser>("AdminUsers", id);
if (adminUser == null)
{
return NotFound();
}
adminUser.Name = user.Name;
adminUser.Email = user.Email;
adminUser.Role = user.Role;
adminUser.Phone = user.Phone;
adminUser.Notes = user.Notes;
adminUser.IsActive = user.IsActive;
adminUser.Permissions = GetRolePermissions(user.Role);
adminUser.PasswordNeverExpires = user.PasswordNeverExpires;
adminUser.PasswordExpiresAt = (user.PasswordNeverExpires ? ((DateTime?)null) : new DateTime?(DateTime.UtcNow.AddDays(90.0)));
if (!string.IsNullOrWhiteSpace(newPassword))
{
adminUser.PasswordHash = _authService.HashPassword(newPassword);
}
await _pgService.UpdateAsync("AdminUsers", id, adminUser);
if (!string.IsNullOrWhiteSpace(newPassword))
{
base.TempData["Success"] = "User " + user.Name + " and password updated successfully!";
}
else
{
base.TempData["Success"] = "User " + user.Name + " updated successfully!";
}
return RedirectToAction("Index");
}
[HttpPost("delete/{id}")]
public async Task<IActionResult> Delete(string id)
{
AdminUser user = await _pgService.GetByIdAsync<AdminUser>("AdminUsers", id);
if (user == null)
{
return NotFound();
}
if (user.Role == "MasterAdmin")
{
base.TempData["Error"] = "Cannot delete Master Admin!";
return RedirectToAction("Index");
}
await _pgService.DeleteAsync<AdminUser>("AdminUsers", id);
base.TempData["Success"] = "User " + user.Name + " deleted successfully!";
return RedirectToAction("Index");
}
[HttpGet("view/{id}")]
public async Task<IActionResult> ViewUser(string id)
{
AdminUser adminUser = await _pgService.GetByIdAsync<AdminUser>("AdminUsers", id);
if (adminUser == null)
{
return NotFound();
}
return View("View", adminUser);
}
private List<string> GetAvailableRoles()
{
return new List<string> { "MasterAdmin", "Admin", "Cashier", "Accountant" };
}
private List<string> GetRolePermissions(string role)
{
return role switch
{
"MasterAdmin" => new List<string>
{
"manage_users", "manage_products", "manage_orders", "manage_content", "manage_settings", "view_reports", "manage_finances", "manage_inventory", "manage_customers", "manage_blog",
"manage_portfolio", "manage_pages"
},
"Admin" => new List<string> { "manage_products", "manage_orders", "manage_content", "view_reports", "manage_inventory", "manage_customers", "manage_blog", "manage_portfolio", "manage_pages" },
"Cashier" => new List<string> { "view_products", "manage_orders", "view_customers", "process_payments" },
"Accountant" => new List<string> { "view_products", "view_orders", "view_reports", "manage_finances", "view_customers", "export_data" },
_ => new List<string>(),
};
}
}

View File

@@ -1,75 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace SkyArtShop.Controllers;
[Route("api/upload")]
[Authorize(Roles = "Admin")]
public class ApiUploadController : Controller
{
private readonly IWebHostEnvironment _environment;
public ApiUploadController(IWebHostEnvironment environment)
{
_environment = environment;
}
[HttpPost("image")]
public async Task<IActionResult> UploadImage(IFormFile image)
{
if (image == null || image.Length == 0L)
{
return Json(new
{
success = false,
message = "No file uploaded"
});
}
string[] source = new string[5] { ".jpg", ".jpeg", ".png", ".gif", ".webp" };
string value = Path.GetExtension(image.FileName).ToLowerInvariant();
if (!source.Contains(value))
{
return Json(new
{
success = false,
message = "Invalid file type. Only images are allowed."
});
}
try
{
string text = Path.Combine(_environment.WebRootPath, "uploads", "images");
if (!Directory.Exists(text))
{
Directory.CreateDirectory(text);
}
string fileName = $"{Guid.NewGuid()}{value}";
string path = Path.Combine(text, fileName);
using (FileStream stream = new FileStream(path, FileMode.Create))
{
await image.CopyToAsync(stream);
}
string text2 = "/uploads/images/" + fileName;
Console.WriteLine("[API-UPLOAD] Image uploaded successfully: " + text2);
return Json(new
{
success = true,
imageUrl = text2
});
}
catch (Exception ex)
{
Console.WriteLine("[API-UPLOAD] Upload failed: " + ex.Message);
return Json(new
{
success = false,
message = "Upload failed: " + ex.Message
});
}
}
}

View File

@@ -1,39 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
public class BlogController : Controller
{
private readonly PostgreSQLService _pgService;
private readonly string _blogCollection = "BlogPosts";
public BlogController(PostgreSQLService pgService)
{
_pgService = pgService;
}
public async Task<IActionResult> Index()
{
List<BlogPost> model = (from p in await _pgService.GetAllAsync<BlogPost>(_blogCollection)
where p.IsPublished
orderby p.PublishedDate descending
select p).ToList();
return View(model);
}
public async Task<IActionResult> Post(string slug)
{
BlogPost blogPost = (await _pgService.GetAllAsync<BlogPost>(_blogCollection)).FirstOrDefault((BlogPost p) => p.Slug == slug && p.IsPublished);
if (blogPost == null)
{
return NotFound();
}
return View(blogPost);
}
}

View File

@@ -1,29 +0,0 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
public class ContactController : Controller
{
private readonly PostgreSQLService _pgService;
public ContactController(PostgreSQLService pgService)
{
_pgService = pgService;
}
public async Task<IActionResult> Index()
{
SiteSettings model = (await _pgService.GetSiteSettingsAsync()) ?? new SiteSettings();
return View(model);
}
[HttpPost]
public IActionResult Submit(string name, string email, string phone, string subject, string message)
{
base.TempData["Success"] = "Thank you! Your message has been sent. We'll get back to you soon.";
return RedirectToAction("Index");
}
}

View File

@@ -1,34 +0,0 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
[Route("diagnostics")]
public class DiagnosticsController : Controller
{
private readonly MongoDBService _mongoService;
public DiagnosticsController(MongoDBService mongoService)
{
_mongoService = mongoService;
}
[HttpGet("products")]
public async Task<IActionResult> Products()
{
var data = (await _mongoService.GetAllAsync<Product>("Products")).Select((Product p) => new
{
Id = p.Id,
Name = p.Name,
ImageUrl = p.ImageUrl,
ImagesCount = (p.Images?.Count ?? 0),
FirstImage = p.Images?.FirstOrDefault(),
HasImageUrl = !string.IsNullOrEmpty(p.ImageUrl),
HasImages = (p.Images != null && p.Images.Any())
}).ToList();
return Json(data);
}
}

View File

@@ -1,67 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
public class HomeController : Controller
{
private readonly PostgreSQLService _pgService;
private readonly string _productsCollection = "Products";
private readonly string _sectionsCollection = "HomepageSections";
public HomeController(PostgreSQLService pgService)
{
_pgService = pgService;
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task<IActionResult> Index()
{
SiteSettings settings = await GetSiteSettings();
List<Product> topProducts = await GetTopSellerProducts();
List<HomepageSection> list = await GetHomepageSections();
base.ViewBag.Settings = settings;
base.ViewBag.TopProducts = topProducts;
base.ViewBag.Sections = list;
return View();
}
private async Task<SiteSettings> GetSiteSettings()
{
return (await _pgService.GetSiteSettingsAsync()) ?? new SiteSettings();
}
private async Task<List<Product>> GetTopSellerProducts()
{
return (await _pgService.GetAllAsync<Product>(_productsCollection)).Where((Product p) => p.IsTopSeller && p.IsActive).Take(4).ToList();
}
private async Task<List<HomepageSection>> GetHomepageSections()
{
List<HomepageSection> list = await _pgService.GetAllAsync<HomepageSection>(_sectionsCollection);
Console.WriteLine($"Total sections from DB: {list.Count}");
List<HomepageSection> list2 = (from s in list
where s.IsActive
orderby s.DisplayOrder
select s).ToList();
Console.WriteLine($"Active sections: {list2.Count}");
foreach (HomepageSection item in list2)
{
Console.WriteLine($"Section: {item.Title} | Type: {item.SectionType} | Order: {item.DisplayOrder} | Active: {item.IsActive}");
}
return list2;
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View();
}
}

View File

@@ -1,29 +0,0 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
[Route("page")]
public class PageController : Controller
{
private readonly PostgreSQLService _pgService;
public PageController(PostgreSQLService pgService)
{
_pgService = pgService;
}
[HttpGet("{slug}")]
public async Task<IActionResult> Index(string slug)
{
Page page = (await _pgService.GetAllAsync<Page>("Pages")).FirstOrDefault((Page p) => p.PageSlug == slug && p.IsActive);
if (page == null)
{
return NotFound();
}
return View("View", page);
}
}

View File

@@ -1,56 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
public class PortfolioController : Controller
{
private readonly PostgreSQLService _pgService;
private readonly string _categoriesCollection = "PortfolioCategories";
private readonly string _projectsCollection = "PortfolioProjects";
public PortfolioController(PostgreSQLService pgService)
{
_pgService = pgService;
}
public async Task<IActionResult> Index()
{
List<PortfolioCategory> model = (from c in await _pgService.GetAllAsync<PortfolioCategory>(_categoriesCollection)
where c.IsActive
orderby c.DisplayOrder
select c).ToList();
return View(model);
}
public async Task<IActionResult> Category(string slug)
{
PortfolioCategory category = (await _pgService.GetAllAsync<PortfolioCategory>(_categoriesCollection)).FirstOrDefault((PortfolioCategory c) => c.Slug == slug && c.IsActive);
if (category == null)
{
return NotFound();
}
List<PortfolioProject> model = (from p in await _pgService.GetAllAsync<PortfolioProject>(_projectsCollection)
where p.CategoryId == category.Id && p.IsActive
orderby p.DisplayOrder
select p).ToList();
base.ViewBag.Category = category;
return View(model);
}
public async Task<IActionResult> Project(string id)
{
PortfolioProject portfolioProject = await _pgService.GetByIdAsync<PortfolioProject>(_projectsCollection, id);
if (portfolioProject == null)
{
return NotFound();
}
return View(portfolioProject);
}
}

View File

@@ -1,72 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers;
public class ShopController : Controller
{
private readonly PostgreSQLService _pgService;
private readonly string _productsCollection = "Products";
public ShopController(PostgreSQLService pgService)
{
_pgService = pgService;
}
public async Task<IActionResult> Index(string? category, string? sort)
{
List<Product> source = (await _pgService.GetAllAsync<Product>(_productsCollection)).Where((Product p) => p.IsActive).ToList();
if (!string.IsNullOrEmpty(category) && category != "all")
{
source = source.Where((Product p) => p.Category == category).ToList();
}
source = sort switch
{
"price-low" => source.OrderBy((Product p) => p.Price).ToList(),
"price-high" => source.OrderByDescending((Product p) => p.Price).ToList(),
"newest" => source.OrderByDescending((Product p) => p.CreatedAt).ToList(),
_ => source.OrderByDescending((Product p) => p.IsFeatured).ToList(),
};
base.ViewBag.SelectedCategory = category ?? "all";
base.ViewBag.SelectedSort = sort ?? "featured";
base.ViewBag.Categories = source.Select((Product p) => p.Category).Distinct().ToList();
return View(source);
}
public async Task<IActionResult> Product(string id)
{
Product product = await _pgService.GetByIdAsync<Product>(_productsCollection, id);
if (product == null)
{
return NotFound();
}
Console.WriteLine("[SHOP] Product ID: " + id);
Console.WriteLine("[SHOP] Product Name: " + product.Name);
Console.WriteLine($"[SHOP] Colors Count: {product.Colors?.Count ?? 0}");
if (product.Colors != null && product.Colors.Any())
{
Console.WriteLine("[SHOP] Colors: " + string.Join(", ", product.Colors));
}
Console.WriteLine("[SHOP] Legacy Color: " + (product.Color ?? "null"));
_ = base.HttpContext.Session.Id;
if (base.HttpContext.Connection.RemoteIpAddress?.ToString() == null)
{
}
List<Product> source = await _pgService.GetAllAsync<Product>(_productsCollection);
List<ProductView> source2 = new List<ProductView>();
Dictionary<string, int> productViewCounts = (from v in source2
group v by v.ProductId).ToDictionary((IGrouping<string, ProductView> g) => g.Key, (IGrouping<string, ProductView> g) => g.Count());
List<Product> list = (from p in source
where p.IsActive && p.Id != id
orderby ((p.Category == product.Category) ? 100 : 0) + (productViewCounts.ContainsKey(p.Id ?? "") ? productViewCounts[p.Id ?? ""] : 0) + (p.IsFeatured ? 50 : 0) + p.UnitsSold * 2 descending
select p).Take(4).ToList();
base.ViewBag.RelatedProducts = list;
return View(product);
}
}

207
DEVELOPMENT_MODE.md Normal file
View File

@@ -0,0 +1,207 @@
# Development Mode - Localhost:5000 Setup
## ✅ Current Setup
### What Changed
- **Nginx**: Stopped and disabled
- **Backend**: Now serves files from development directory
- **Port**: Website runs on `http://localhost:5000`
- **Auto-reload**: PM2 watch mode enabled for instant changes
### File Locations
- **Development files**: `/media/pts/Website/SkyArtShop/website/`
- Public pages: `website/public/`
- Admin panel: `website/admin/`
- Assets: `website/assets/`
- Uploads: `website/uploads/`
### How It Works
```javascript
// Backend automatically serves from development directory
const baseDir = path.join(__dirname, "..", "website");
app.use(express.static(path.join(baseDir, "public")));
app.use("/admin", express.static(path.join(baseDir, "admin")));
```
## 🚀 Access Your Site
### URLs
- **Homepage**: <http://localhost:5000/>
- **Admin Login**: <http://localhost:5000/admin/login.html>
- **Admin Dashboard**: <http://localhost:5000/admin/dashboard.html>
- **Any page**: <http://localhost:5000/[page-name].html>
### API Endpoints
- **Admin API**: <http://localhost:5000/api/admin/>*
- **Public API**: <http://localhost:5000/api/>*
- **Health Check**: <http://localhost:5000/health>
## 🔄 Instant Reflection of Changes
### PM2 Watch Mode Enabled
PM2 is watching for changes and will auto-restart the backend when:
- Backend files change (routes, server.js, etc.)
- Configuration changes
### Frontend Changes (HTML/CSS/JS)
Frontend files are served directly from `/media/pts/Website/SkyArtShop/website/`
- Just **refresh your browser** (F5 or Ctrl+R)
- Changes show immediately - no deployment needed!
- No need to restart anything
### Backend Changes
- PM2 watch mode automatically restarts the server
- Changes apply within 1-2 seconds
## 📝 Development Workflow
### Making Changes
1. **Edit any file in `/media/pts/Website/SkyArtShop/website/`**
```bash
# Example: Edit homepage
nano /media/pts/Website/SkyArtShop/website/public/home.html
# Example: Edit admin dashboard
nano /media/pts/Website/SkyArtShop/website/admin/dashboard.html
```
2. **Refresh browser** - Changes show immediately!
3. **No deployment needed** - You're working directly with source files
### Checking Status
```bash
# Check if backend is running
pm2 status
# View logs
pm2 logs skyartshop
# Restart manually if needed
pm2 restart skyartshop
```
## 🛠️ Useful Commands
### Backend Management
```bash
# View PM2 status
pm2 status
# View real-time logs
pm2 logs skyartshop --lines 50
# Restart backend
pm2 restart skyartshop
# Stop backend
pm2 stop skyartshop
# Start backend
pm2 start skyartshop
```
### Test Endpoints
```bash
# Test homepage
curl http://localhost:5000/
# Test admin login
curl http://localhost:5000/admin/login.html
# Test API
curl http://localhost:5000/api/products
# Test health
curl http://localhost:5000/health
```
## 🌐 When Ready to Push Live
### Re-enable Nginx for Production
```bash
# 1. Deploy files to production
sudo ./deploy-website.sh
# 2. Update backend to production mode
# Edit backend/server.js: Set NODE_ENV=production
# 3. Restart backend
pm2 restart skyartshop
# 4. Enable and start nginx
sudo systemctl enable nginx
sudo systemctl start nginx
```
### Or Use Deployment Script
```bash
# Deploy everything at once
sudo ./deploy-website.sh
```
## 📊 Current Configuration
### Backend (server.js)
- **Environment**: Development (auto-detected)
- **Port**: 5000
- **Static Files**: `/media/pts/Website/SkyArtShop/website/`
- **Watch Mode**: Enabled
- **Session Cookie**: secure=false (for HTTP)
### Nginx
- **Status**: Stopped and disabled
- **Will be re-enabled**: When pushing to production
### Database
- **PostgreSQL**: Still connected and working
- **Session Store**: Active
- **All data preserved**: No changes to database
## ✨ Benefits
**No deployment needed** - Work directly with source files
**Instant changes** - Just refresh browser
**Auto-restart** - PM2 watches for backend changes
**No HTTPS complexity** - Simple HTTP development
**Database intact** - All your data is safe
**Easy testing** - One URL: localhost:5000
## 🎯 Summary
**Before:**
- Nginx on port 80/443 → Served from /var/www/skyartshop/
- Had to deploy every change
- Two different sites (localhost vs domain)
**Now:**
- Backend on port 5000 → Serves from /media/pts/Website/SkyArtShop/website/
- Edit files directly, refresh browser
- One development site on localhost:5000
- Ready to push to production when done
---
**Access your site now at: <http://localhost:5000>** 🚀

View File

@@ -0,0 +1,96 @@
# ═══════════════════════════════════════════════════════════════
# Windows Localhost Disabler Script
# Run this in PowerShell as Administrator on your WINDOWS machine
# ═══════════════════════════════════════════════════════════════
Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan
Write-Host "Disabling Web Servers on Windows" -ForegroundColor Yellow
Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan
Write-Host ""
# Check if running as Administrator
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$isAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Write-Host "❌ ERROR: You must run PowerShell as Administrator!" -ForegroundColor Red
Write-Host "Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
pause
exit
}
Write-Host "✅ Running as Administrator" -ForegroundColor Green
Write-Host ""
# Function to stop and disable service
function Stop-AndDisableService {
param($serviceName)
$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if ($service) {
Write-Host "Found: $serviceName" -ForegroundColor Yellow
if ($service.Status -eq 'Running') {
Write-Host " Stopping $serviceName..." -ForegroundColor Cyan
Stop-Service -Name $serviceName -Force
Write-Host " ✅ Stopped" -ForegroundColor Green
}
Write-Host " Disabling $serviceName..." -ForegroundColor Cyan
Set-Service -Name $serviceName -StartupType Disabled
Write-Host " ✅ Disabled" -ForegroundColor Green
} else {
Write-Host "$serviceName not found (skip)" -ForegroundColor Gray
}
Write-Host ""
}
# Stop IIS
Write-Host "─────────────────────────────────────────────" -ForegroundColor Cyan
Write-Host "Checking IIS (Internet Information Services)" -ForegroundColor Yellow
Write-Host "─────────────────────────────────────────────" -ForegroundColor Cyan
Stop-AndDisableService "W3SVC"
Stop-AndDisableService "WAS"
Stop-AndDisableService "IISADMIN"
# Stop Apache
Write-Host "─────────────────────────────────────────────" -ForegroundColor Cyan
Write-Host "Checking Apache Web Server" -ForegroundColor Yellow
Write-Host "─────────────────────────────────────────────" -ForegroundColor Cyan
Stop-AndDisableService "Apache2.4"
Stop-AndDisableService "wampapache64"
Stop-AndDisableService "xamppApache"
# Check what's on port 80
Write-Host "─────────────────────────────────────────────" -ForegroundColor Cyan
Write-Host "Checking Port 80" -ForegroundColor Yellow
Write-Host "─────────────────────────────────────────────" -ForegroundColor Cyan
$port80 = Get-NetTCPConnection -LocalPort 80 -ErrorAction SilentlyContinue
if ($port80) {
Write-Host "⚠️ Something is still using port 80:" -ForegroundColor Yellow
foreach ($conn in $port80) {
$process = Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue
Write-Host " PID: $($conn.OwningProcess) - $($process.Name)" -ForegroundColor Red
$response = Read-Host "Kill this process? (Y/N)"
if ($response -eq 'Y' -or $response -eq 'y') {
Stop-Process -Id $conn.OwningProcess -Force
Write-Host " ✅ Killed process $($process.Name)" -ForegroundColor Green
}
}
} else {
Write-Host "✅ Port 80 is FREE!" -ForegroundColor Green
}
Write-Host ""
Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan
Write-Host "✅ DONE!" -ForegroundColor Green
Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan
Write-Host ""
Write-Host "Now try in Firefox:" -ForegroundColor Yellow
Write-Host " http://localhost:5000/" -ForegroundColor Cyan
Write-Host ""
Write-Host "Or:" -ForegroundColor Yellow
Write-Host " http://192.168.10.130:5000/" -ForegroundColor Cyan
Write-Host ""
pause

View File

@@ -0,0 +1,71 @@
═══════════════════════════════════════════════════════════════
HOW TO DISABLE LOCALHOST ON WINDOWS
═══════════════════════════════════════════════════════════════
On your WINDOWS machine, open PowerShell as Administrator:
(Right-click Start Menu → Windows PowerShell (Admin))
Then run these commands:
════════════════════════════════════════════════════════════════
STEP 1: Stop IIS (if installed)
════════════════════════════════════════════════════════════════
iisreset /stop
════════════════════════════════════════════════════════════════
STEP 2: Disable IIS from starting automatically
════════════════════════════════════════════════════════════════
Set-Service -Name W3SVC -StartupType Disabled
Set-Service -Name WAS -StartupType Disabled
════════════════════════════════════════════════════════════════
STEP 3: Stop Apache/XAMPP/WAMP (if installed)
════════════════════════════════════════════════════════════════
net stop Apache2.4
# OR
net stop wampapache64
════════════════════════════════════════════════════════════════
STEP 4: Check what's running on port 80
════════════════════════════════════════════════════════════════
netstat -ano | findstr :80
# This will show you what's using port 80
# Look for the PID (last number in the line)
════════════════════════════════════════════════════════════════
STEP 5: Kill the process using port 80
════════════════════════════════════════════════════════════════
# Replace XXXX with the PID from Step 4
taskkill /PID XXXX /F
════════════════════════════════════════════════════════════════
ALTERNATIVE: Use GUI
════════════════════════════════════════════════════════════════
1. Press Windows + R
2. Type: services.msc
3. Press Enter
4. Find these services and STOP + DISABLE them:
- World Wide Web Publishing Service (W3SVC)
- Windows Process Activation Service (WAS)
- Apache2.4 (if exists)
Right-click each → Stop
Right-click each → Properties → Startup type: Disabled → OK
════════════════════════════════════════════════════════════════
AFTER DISABLING:
════════════════════════════════════════════════════════════════
In Firefox, go to:
http://localhost:5000/
This will now connect to your Linux server!
═══════════════════════════════════════════════════════════════

View File

@@ -1,12 +0,0 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace SkyArtShop.Data;
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base((DbContextOptions)options)
{
}
}

View File

@@ -1,8 +0,0 @@
using Microsoft.AspNetCore.Identity;
namespace SkyArtShop.Data;
public class ApplicationUser : IdentityUser
{
public string DisplayName { get; set; } = string.Empty;
}

View File

@@ -1,172 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SkyArtShop.Models;
namespace SkyArtShop.Data;
public class SkyArtShopDbContext : DbContext
{
public DbSet<Page> Pages { get; set; }
public DbSet<PortfolioCategory> PortfolioCategories { get; set; }
public DbSet<PortfolioProject> PortfolioProjects { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<BlogPost> BlogPosts { get; set; }
public DbSet<SiteSettings> SiteSettings { get; set; }
public DbSet<MenuItem> MenuItems { get; set; }
public DbSet<AdminUser> AdminUsers { get; set; }
public DbSet<UserRole> UserRoles { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<ProductView> ProductViews { get; set; }
public DbSet<HomepageSection> HomepageSections { get; set; }
public SkyArtShopDbContext(DbContextOptions<SkyArtShopDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity(delegate(EntityTypeBuilder<Page> entity)
{
entity.HasKey((Page e) => e.Id);
entity.Property((Page e) => e.Id).ValueGeneratedOnAdd();
entity.Property((Page e) => e.PageName).IsRequired().HasMaxLength(200);
entity.Property((Page e) => e.PageSlug).IsRequired().HasMaxLength(200);
entity.HasIndex((Page e) => e.PageSlug).IsUnique();
entity.Property((Page e) => e.Content).HasColumnType("text");
entity.Property((Page e) => e.ImageGallery).HasColumnType("jsonb");
entity.OwnsMany((Page e) => e.TeamMembers, delegate(OwnedNavigationBuilder<Page, TeamMember> tm)
{
tm.Property((TeamMember t) => t.Name).HasMaxLength(200);
tm.Property((TeamMember t) => t.Role).HasMaxLength(200);
tm.Property((TeamMember t) => t.Bio).HasColumnType("text");
});
});
modelBuilder.Entity(delegate(EntityTypeBuilder<PortfolioCategory> entity)
{
entity.HasKey((PortfolioCategory e) => e.Id);
entity.Property((PortfolioCategory e) => e.Id).ValueGeneratedOnAdd();
entity.Property((PortfolioCategory e) => e.Name).IsRequired().HasMaxLength(200);
entity.Property((PortfolioCategory e) => e.Slug).IsRequired().HasMaxLength(200);
entity.HasIndex((PortfolioCategory e) => e.Slug).IsUnique();
});
modelBuilder.Entity(delegate(EntityTypeBuilder<PortfolioProject> entity)
{
entity.HasKey((PortfolioProject e) => e.Id);
entity.Property((PortfolioProject e) => e.Id).ValueGeneratedOnAdd();
entity.Property((PortfolioProject e) => e.Title).IsRequired().HasMaxLength(300);
entity.Property((PortfolioProject e) => e.CategoryId).IsRequired().HasMaxLength(50);
entity.Property((PortfolioProject e) => e.Description).HasColumnType("text");
entity.Property((PortfolioProject e) => e.Images).HasColumnType("jsonb");
});
modelBuilder.Entity(delegate(EntityTypeBuilder<Product> entity)
{
entity.HasKey((Product e) => e.Id);
entity.Property((Product e) => e.Id).ValueGeneratedOnAdd();
entity.Property((Product e) => e.Name).IsRequired().HasMaxLength(300);
entity.Property((Product e) => e.Slug).IsRequired().HasMaxLength(300);
entity.HasIndex((Product e) => e.Slug).IsUnique();
entity.Property((Product e) => e.SKU).HasMaxLength(100);
entity.Property((Product e) => e.Description).HasColumnType("text");
entity.Property((Product e) => e.Price).HasColumnType("numeric(18,2)");
entity.Property((Product e) => e.Colors).HasColumnType("jsonb");
entity.Property((Product e) => e.Images).HasColumnType("jsonb");
entity.Property((Product e) => e.Tags).HasColumnType("jsonb");
entity.Property((Product e) => e.TotalRevenue).HasColumnType("numeric(18,2)");
entity.Property((Product e) => e.CostPrice).HasColumnType("numeric(18,2)");
entity.OwnsMany((Product e) => e.Variants, delegate(OwnedNavigationBuilder<Product, ProductVariant> v)
{
v.Property((ProductVariant pv) => pv.ColorName).HasMaxLength(100);
v.Property((ProductVariant pv) => pv.ColorHex).HasMaxLength(20);
v.Property((ProductVariant pv) => pv.Images).HasColumnType("jsonb");
v.Property((ProductVariant pv) => pv.SKU).HasMaxLength(100);
v.Property((ProductVariant pv) => pv.PriceAdjustment).HasColumnType("numeric(18,2)");
});
});
modelBuilder.Entity(delegate(EntityTypeBuilder<BlogPost> entity)
{
entity.HasKey((BlogPost e) => e.Id);
entity.Property((BlogPost e) => e.Id).ValueGeneratedOnAdd();
entity.Property((BlogPost e) => e.Title).IsRequired().HasMaxLength(300);
entity.Property((BlogPost e) => e.Slug).IsRequired().HasMaxLength(300);
entity.HasIndex((BlogPost e) => e.Slug).IsUnique();
entity.Property((BlogPost e) => e.Content).HasColumnType("text");
entity.Property((BlogPost e) => e.Tags).HasColumnType("jsonb");
});
modelBuilder.Entity(delegate(EntityTypeBuilder<SiteSettings> entity)
{
entity.HasKey((SiteSettings e) => e.Id);
entity.Property((SiteSettings e) => e.Id).ValueGeneratedOnAdd();
});
modelBuilder.Entity(delegate(EntityTypeBuilder<MenuItem> entity)
{
entity.HasKey((MenuItem e) => e.Id);
entity.Property((MenuItem e) => e.Id).ValueGeneratedOnAdd();
entity.Property((MenuItem e) => e.Label).IsRequired().HasMaxLength(200);
entity.Property((MenuItem e) => e.Url).IsRequired().HasMaxLength(500);
});
modelBuilder.Entity(delegate(EntityTypeBuilder<AdminUser> entity)
{
entity.HasKey((AdminUser e) => e.Id);
entity.Property((AdminUser e) => e.Id).ValueGeneratedOnAdd();
entity.Property((AdminUser e) => e.Email).IsRequired().HasMaxLength(256);
entity.HasIndex((AdminUser e) => e.Email).IsUnique();
entity.Property((AdminUser e) => e.PasswordHash).IsRequired().HasMaxLength(500);
entity.Property((AdminUser e) => e.Name).IsRequired().HasMaxLength(200);
entity.Property((AdminUser e) => e.Role).IsRequired().HasMaxLength(100);
entity.Property((AdminUser e) => e.Permissions).HasColumnType("jsonb");
});
modelBuilder.Entity(delegate(EntityTypeBuilder<UserRole> entity)
{
entity.HasKey((UserRole e) => e.Id);
entity.Property((UserRole e) => e.Id).ValueGeneratedOnAdd();
entity.Property((UserRole e) => e.RoleName).IsRequired().HasMaxLength(100);
entity.HasIndex((UserRole e) => e.RoleName).IsUnique();
entity.Property((UserRole e) => e.Permissions).HasColumnType("jsonb");
});
modelBuilder.Entity(delegate(EntityTypeBuilder<Order> entity)
{
entity.HasKey((Order e) => e.Id);
entity.Property((Order e) => e.Id).ValueGeneratedOnAdd();
entity.Property((Order e) => e.CustomerEmail).HasMaxLength(256);
entity.Property((Order e) => e.CustomerName).HasMaxLength(200);
entity.Property((Order e) => e.TotalAmount).HasColumnType("numeric(18,2)");
entity.OwnsMany((Order e) => e.Items, delegate(OwnedNavigationBuilder<Order, OrderItem> oi)
{
oi.Property((OrderItem o) => o.ProductId).HasMaxLength(50);
oi.Property((OrderItem o) => o.ProductName).HasMaxLength(300);
oi.Property((OrderItem o) => o.SKU).HasMaxLength(100);
oi.Property((OrderItem o) => o.Price).HasColumnType("numeric(18,2)");
oi.Property((OrderItem o) => o.Subtotal).HasColumnType("numeric(18,2)");
});
});
modelBuilder.Entity(delegate(EntityTypeBuilder<ProductView> entity)
{
entity.HasKey((ProductView e) => e.Id);
entity.Property((ProductView e) => e.Id).ValueGeneratedOnAdd();
entity.Property((ProductView e) => e.ProductId).HasMaxLength(50);
entity.Property((ProductView e) => e.SessionId).HasMaxLength(200);
entity.Property((ProductView e) => e.IpAddress).HasMaxLength(50);
});
modelBuilder.Entity(delegate(EntityTypeBuilder<HomepageSection> entity)
{
entity.HasKey((HomepageSection e) => e.Id);
entity.Property((HomepageSection e) => e.Id).ValueGeneratedOnAdd();
entity.Property((HomepageSection e) => e.SectionType).HasMaxLength(100);
entity.Property((HomepageSection e) => e.Content).HasColumnType("text");
entity.Property((HomepageSection e) => e.AdditionalData).HasColumnType("jsonb");
});
}
}

View File

@@ -1,45 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace SkyArtShop.Models;
public class AdminUser
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
public string PasswordHash { get; set; } = string.Empty;
[Required]
public string Name { get; set; } = string.Empty;
[Required]
public string Role { get; set; } = "Admin";
public List<string> Permissions { get; set; } = new List<string>();
public bool IsActive { get; set; } = true;
public bool PasswordNeverExpires { get; set; } = true;
public DateTime? PasswordExpiresAt { get; set; }
public string CreatedBy { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? LastLogin { get; set; }
public string Phone { get; set; } = string.Empty;
public string Notes { get; set; } = string.Empty;
}

View File

@@ -1,38 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace SkyArtShop.Models;
public class BlogPost
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[Required]
public string Title { get; set; } = string.Empty;
[Required]
public string Slug { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string Excerpt { get; set; } = string.Empty;
public string FeaturedImage { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public List<string> Tags { get; set; } = new List<string>();
public bool IsPublished { get; set; } = true;
public DateTime PublishedDate { get; set; } = DateTime.UtcNow;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,10 +0,0 @@
namespace SkyArtShop.Models;
public class CollectionItem
{
public string Title { get; set; } = string.Empty;
public string ImageUrl { get; set; } = string.Empty;
public string Link { get; set; } = string.Empty;
}

View File

@@ -1,37 +0,0 @@
using System;
using System.Collections.Generic;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace SkyArtShop.Models;
public class HomepageSection
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
public string SectionType { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Subtitle { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string ImageUrl { get; set; } = string.Empty;
public string ButtonText { get; set; } = string.Empty;
public string ButtonUrl { get; set; } = string.Empty;
public int DisplayOrder { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public Dictionary<string, string> AdditionalData { get; set; } = new Dictionary<string, string>();
}

View File

@@ -1,31 +0,0 @@
using System;
using System.ComponentModel.DataAnnotations;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace SkyArtShop.Models;
public class MenuItem
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[Required]
public string Label { get; set; } = string.Empty;
[Required]
public string Url { get; set; } = string.Empty;
public int DisplayOrder { get; set; }
public bool IsActive { get; set; } = true;
public bool ShowInNavbar { get; set; } = true;
public bool ShowInDropdown { get; set; } = true;
public bool OpenInNewTab { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,27 +0,0 @@
using System;
using System.Collections.Generic;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace SkyArtShop.Models;
public class Order
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
public string CustomerEmail { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public List<OrderItem> Items { get; set; } = new List<OrderItem>();
public decimal TotalAmount { get; set; }
public string Status { get; set; } = "Pending";
public DateTime OrderDate { get; set; } = DateTime.UtcNow;
public DateTime? CompletedDate { get; set; }
}

View File

@@ -1,16 +0,0 @@
namespace SkyArtShop.Models;
public class OrderItem
{
public string ProductId { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
public string SKU { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal Subtotal { get; set; }
}

View File

@@ -1,46 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace SkyArtShop.Models;
public class Page
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string? Id { get; set; }
[Required]
public string PageName { get; set; } = string.Empty;
public string PageSlug { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Subtitle { get; set; } = string.Empty;
public string HeroImage { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string MetaDescription { get; set; } = string.Empty;
public List<string> ImageGallery { get; set; } = new List<string>();
public string AboutImage1 { get; set; } = string.Empty;
public string AboutImage2 { get; set; } = string.Empty;
public List<TeamMember> TeamMembers { get; set; } = new List<TeamMember>();
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,33 +0,0 @@
using System;
using System.ComponentModel.DataAnnotations;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace SkyArtShop.Models;
public class PortfolioCategory
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[Required]
public string Name { get; set; } = string.Empty;
[Required]
public string Slug { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string ThumbnailImage { get; set; } = string.Empty;
public string FeaturedImage { get; set; } = string.Empty;
public int DisplayOrder { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace SkyArtShop.Models;
public class PortfolioProject
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[Required]
public string Title { get; set; } = string.Empty;
[Required]
public string CategoryId { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string FeaturedImage { get; set; } = string.Empty;
public List<string> Images { get; set; } = new List<string>();
public string ProjectDate { get; set; } = string.Empty;
public int DisplayOrder { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,66 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace SkyArtShop.Models;
public class Product
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[Required]
public string Name { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public string SKU { get; set; } = string.Empty;
public string ShortDescription { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
[Required]
public decimal Price { get; set; }
public string Category { get; set; } = string.Empty;
public string Color { get; set; } = string.Empty;
public List<string> Colors { get; set; } = new List<string>();
public List<ProductVariant> Variants { get; set; } = new List<ProductVariant>();
public string ImageUrl { get; set; } = string.Empty;
public List<string> Images { get; set; } = new List<string>();
public bool IsFeatured { get; set; }
public bool IsTopSeller { get; set; }
public int StockQuantity { get; set; }
public bool IsActive { get; set; } = true;
public int UnitsSold { get; set; }
public decimal TotalRevenue { get; set; }
public double AverageRating { get; set; }
public int TotalReviews { get; set; }
public decimal CostPrice { get; set; }
public List<string> Tags { get; set; } = new List<string>();
public string MetaDescription { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,20 +0,0 @@
using System.Collections.Generic;
namespace SkyArtShop.Models;
public class ProductVariant
{
public string ColorName { get; set; } = string.Empty;
public string ColorHex { get; set; } = string.Empty;
public List<string> Images { get; set; } = new List<string>();
public int StockQuantity { get; set; }
public decimal? PriceAdjustment { get; set; }
public bool IsAvailable { get; set; } = true;
public string SKU { get; set; } = string.Empty;
}

View File

@@ -1,20 +0,0 @@
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace SkyArtShop.Models;
public class ProductView
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
public string ProductId { get; set; } = string.Empty;
public string SessionId { get; set; } = string.Empty;
public string IpAddress { get; set; } = string.Empty;
public DateTime ViewedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,14 +0,0 @@
namespace SkyArtShop.Models;
public class PromotionCard
{
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string ButtonText { get; set; } = string.Empty;
public string ButtonUrl { get; set; } = string.Empty;
public bool IsFeatured { get; set; }
}

View File

@@ -1,27 +0,0 @@
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace SkyArtShop.Models;
[BsonIgnoreExtraElements]
public class SiteSettings
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
public string SiteName { get; set; } = "Sky Art Shop";
public string SiteTagline { get; set; } = "Scrapbooking and Journaling Fun";
public string ContactEmail { get; set; } = "info@skyartshop.com";
public string ContactPhone { get; set; } = "+501 608-0409";
public string InstagramUrl { get; set; } = "#";
public string FooterText { get; set; } = "© 2035 by Sky Art Shop. Powered and secured by Wix";
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,12 +0,0 @@
namespace SkyArtShop.Models;
public class TeamMember
{
public string Name { get; set; } = string.Empty;
public string Role { get; set; } = string.Empty;
public string Bio { get; set; } = string.Empty;
public string PhotoUrl { get; set; } = string.Empty;
}

View File

@@ -1,25 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace SkyArtShop.Models;
public class UserRole
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[Required]
public string RoleName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public List<string> Permissions { get; set; } = new List<string>();
public bool IsSystemRole { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,358 @@
# PostgreSQL Database Integration - Complete ✅
## Overview
All backend data is now being recorded to PostgreSQL for proper database management. This includes file uploads, products, blog posts, portfolio items, and all other content.
## What Changed
### 1. Uploads Table Created
**Location:** PostgreSQL database `skyartshop`
**Schema:**
```sql
CREATE TABLE uploads (
id SERIAL PRIMARY KEY,
filename VARCHAR(255) UNIQUE NOT NULL,
original_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_size INTEGER NOT NULL,
mime_type VARCHAR(100) NOT NULL,
uploaded_by INTEGER,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
used_in_type VARCHAR(50), -- e.g., 'product', 'blog', 'portfolio'
used_in_id INTEGER -- ID of the item using this image
);
CREATE INDEX idx_uploads_filename ON uploads(filename);
CREATE INDEX idx_uploads_created_at ON uploads(created_at DESC);
```
### 2. Upload Routes Updated
**File:** `/backend/routes/upload.js`
**Changes:**
- ✅ POST /api/admin/upload - Now inserts records into PostgreSQL
- ✅ GET /api/admin/uploads - Now queries from PostgreSQL instead of filesystem
- ✅ DELETE /api/admin/uploads/:filename - Now deletes from both database and disk
**Key Features:**
- Tracks who uploaded each file (`uploaded_by` field)
- Records file metadata (size, type, original name)
- Maintains usage tracking (`used_in_type`, `used_in_id`)
- Cleans up files if database insert fails (rollback)
- Deletes from database first, then file (safe deletion)
### 3. Database Integration Flow
#### Upload Process
```
User uploads file → Multer saves to disk → Insert record to PostgreSQL → Return file info
↓ (if fails)
Delete file from disk
```
#### List Process
```
User requests files → Query PostgreSQL uploads table → Return sorted results
```
#### Delete Process
```
User deletes file → Delete from PostgreSQL → Delete from disk → Return success
```
## API Endpoints
### POST /api/admin/upload
**Purpose:** Upload multiple files and record in database
**Request:**
- Method: POST
- Content-Type: multipart/form-data
- Body: files[] (up to 10 files, 5MB each)
- Auth: Required (session)
**Response:**
```json
{
"success": true,
"message": "2 file(s) uploaded successfully",
"files": [
{
"id": 1,
"filename": "product-image-1234567890-123456789.jpg",
"originalName": "Product Image.jpg",
"size": 245678,
"mimetype": "image/jpeg",
"path": "/uploads/product-image-1234567890-123456789.jpg",
"uploadDate": "2025-12-14T08:30:15.234Z"
}
]
}
```
### GET /api/admin/uploads
**Purpose:** List all uploaded files from database
**Request:**
- Method: GET
- Auth: Required (session)
**Response:**
```json
{
"success": true,
"files": [
{
"id": 1,
"filename": "product-image-1234567890-123456789.jpg",
"originalName": "Product Image.jpg",
"size": 245678,
"mimetype": "image/jpeg",
"path": "/uploads/product-image-1234567890-123456789.jpg",
"uploadDate": "2025-12-14T08:30:15.234Z",
"uploadedBy": 1,
"usedInType": "product",
"usedInId": 42
}
]
}
```
### DELETE /api/admin/uploads/:filename
**Purpose:** Delete file from both database and disk
**Request:**
- Method: DELETE
- URL: /api/admin/uploads/product-image-1234567890-123456789.jpg
- Auth: Required (session)
**Response:**
```json
{
"success": true,
"message": "File deleted successfully"
}
```
## Database Schema Details
### Field Descriptions
| Field | Type | Description |
|-------|------|-------------|
| id | SERIAL | Primary key, auto-increment |
| filename | VARCHAR(255) | Unique system filename (sanitized + timestamp) |
| original_name | VARCHAR(255) | Original filename from user |
| file_path | VARCHAR(500) | Web-accessible path (e.g., /uploads/...) |
| file_size | INTEGER | File size in bytes |
| mime_type | VARCHAR(100) | MIME type (e.g., image/jpeg) |
| uploaded_by | INTEGER | FK to adminusers.id (nullable) |
| created_at | TIMESTAMP | Upload timestamp |
| updated_at | TIMESTAMP | Last modification timestamp |
| used_in_type | VARCHAR(50) | Content type using this file |
| used_in_id | INTEGER | ID of content using this file |
### Indexes
- `uploads_pkey`: Primary key on id
- `uploads_filename_key`: Unique constraint on filename
- `idx_uploads_filename`: B-tree index for filename lookups
- `idx_uploads_created_at`: B-tree index for sorting by date (DESC)
## Testing
### Test Database Integration
```bash
cd /media/pts/Website/SkyArtShop/backend
node test-upload-db.js
```
**Test Coverage:**
1. ✅ Uploads table exists
2. ✅ Table structure verified (11 columns)
3. ✅ Indexes created (4 indexes)
4. ✅ Query existing uploads
5. ✅ Foreign key constraints checked
### Test Upload Flow
1. Open media library: <http://localhost:5000/admin/media-library.html>
2. Upload a test image
3. Check database:
```bash
cd /media/pts/Website/SkyArtShop/backend
node -e "const {pool}=require('./config/database');(async()=>{const r=await pool.query('SELECT * FROM uploads ORDER BY created_at DESC LIMIT 5');console.table(r.rows);pool.end();})()"
```
4. Verify file exists in `/website/uploads/`
5. Delete file from media library
6. Verify removed from both database and disk
## Security Features
### File Upload Security
- ✅ Only authenticated users can upload
- ✅ File type validation (images only)
- ✅ File size limit (5MB)
- ✅ Filename sanitization
- ✅ Path traversal protection
- ✅ Unique filename generation
### Database Security
- ✅ Parameterized queries (SQL injection prevention)
- ✅ User attribution (uploaded_by tracking)
- ✅ Foreign key constraints (data integrity)
- ✅ Unique filename constraint (no duplicates)
### Deletion Security
- ✅ Path validation (must be within uploads directory)
- ✅ Database-first deletion (prevents orphaned files)
- ✅ Safe error handling (continues if file already deleted)
## Usage Tracking
### Future Feature: Track Image Usage
The `used_in_type` and `used_in_id` fields allow tracking where each image is used:
**Example:**
```javascript
// When assigning image to product
await pool.query(
"UPDATE uploads SET used_in_type = $1, used_in_id = $2 WHERE filename = $3",
['product', productId, filename]
);
// Find all images used in products
const productImages = await pool.query(
"SELECT * FROM uploads WHERE used_in_type = 'product'"
);
// Find unused images
const unusedImages = await pool.query(
"SELECT * FROM uploads WHERE used_in_type IS NULL"
);
```
## Admin Panel Integration
### Next Steps
1. **Products Page** - Add "Browse Images" button to open media library
2. **Blog Page** - Integrate image selection for featured images
3. **Portfolio Page** - Integrate image gallery selection
4. **Pages CMS** - Integrate image picker for page content
### Integration Pattern
```javascript
// In admin form JavaScript
function openMediaLibrary() {
const popup = window.open(
'/admin/media-library.html',
'mediaLibrary',
'width=1200,height=800'
);
}
// Receive selected images
window.receiveMediaFiles = function(selectedFiles) {
selectedFiles.forEach(file => {
console.log('Selected:', file.path);
// Update form input with file.path
});
};
```
## File Structure
```
backend/
├── routes/
│ └── upload.js # ✅ PostgreSQL integrated
├── config/
│ └── database.js # PostgreSQL connection pool
├── uploads-schema.sql # Schema definition
├── test-upload-db.js # Test script
└── server.js # Mounts upload routes
website/
├── uploads/ # Physical file storage
└── admin/
├── media-library.html # Media manager UI
├── products.html # Needs integration
├── blog.html # Needs integration
└── portfolio.html # Needs integration
```
## Maintenance
### Check Upload Statistics
```bash
node -e "const {pool}=require('./config/database');(async()=>{const r=await pool.query('SELECT COUNT(*) as total, SUM(file_size) as total_size FROM uploads');console.log('Total uploads:',r.rows[0].total);console.log('Total size:',(r.rows[0].total_size/1024/1024).toFixed(2)+'MB');pool.end();})()"
```
### Find Large Files
```bash
node -e "const {pool}=require('./config/database');(async()=>{const r=await pool.query('SELECT filename, file_size, created_at FROM uploads WHERE file_size > 1048576 ORDER BY file_size DESC LIMIT 10');console.table(r.rows);pool.end();})()"
```
### Find Unused Images
```bash
node -e "const {pool}=require('./config/database');(async()=>{const r=await pool.query('SELECT filename, original_name, created_at FROM uploads WHERE used_in_type IS NULL ORDER BY created_at DESC');console.table(r.rows);pool.end();})()"
```
## Status
**COMPLETE** - All backend data is now recorded to PostgreSQL
- Uploads table created with proper schema
- Upload routes integrated with database
- File tracking with user attribution
- Usage tracking fields for future features
- Security measures implemented
- Test suite available
- Documentation complete
## Next Phase
Move to admin panel integration:
1. Add "Browse Images" buttons to all admin forms
2. Connect media library popup to forms
3. Implement image selection callbacks
4. Add edit/delete/add functionality to all admin sections
---
**Last Updated:** December 14, 2025
**Status:** ✅ Production Ready

View File

@@ -1,134 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.Tasks;
using SkyArtShop.Models;
namespace SkyArtShop.Services;
public class AuthService
{
private readonly MongoDBService _mongoService;
public AuthService(MongoDBService mongoService)
{
_mongoService = mongoService;
}
public string HashPassword(string password)
{
using RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create();
byte[] array = new byte[16];
randomNumberGenerator.GetBytes(array);
using Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, array, 10000, HashAlgorithmName.SHA256);
byte[] bytes = rfc2898DeriveBytes.GetBytes(32);
byte[] array2 = new byte[48];
Array.Copy(array, 0, array2, 0, 16);
Array.Copy(bytes, 0, array2, 16, 32);
return Convert.ToBase64String(array2);
}
public bool VerifyPassword(string password, string hashedPassword)
{
try
{
byte[] array = Convert.FromBase64String(hashedPassword);
byte[] array2 = new byte[16];
Array.Copy(array, 0, array2, 0, 16);
using Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, array2, 10000, HashAlgorithmName.SHA256);
byte[] bytes = rfc2898DeriveBytes.GetBytes(32);
for (int i = 0; i < 32; i++)
{
if (array[i + 16] != bytes[i])
{
return false;
}
}
return true;
}
catch
{
return false;
}
}
public async Task<AdminUser?> AuthenticateAsync(string email, string password)
{
AdminUser user = (await _mongoService.GetAllAsync<AdminUser>("AdminUsers")).FirstOrDefault((AdminUser u) => u.Email.ToLower() == email.ToLower() && u.IsActive);
if (user == null || !VerifyPassword(password, user.PasswordHash))
{
return null;
}
user.LastLogin = DateTime.UtcNow;
await _mongoService.UpdateAsync("AdminUsers", user.Id, user);
return user;
}
public ClaimsPrincipal CreateClaimsPrincipal(AdminUser user)
{
List<Claim> list = new List<Claim>
{
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", user.Id),
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", user.Email),
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", user.Name),
new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", user.Role)
};
foreach (string permission in user.Permissions)
{
list.Add(new Claim("Permission", permission));
}
ClaimsIdentity identity = new ClaimsIdentity(list, "Cookies");
return new ClaimsPrincipal(identity);
}
public async Task<AdminUser?> GetUserByIdAsync(string userId)
{
return await _mongoService.GetByIdAsync<AdminUser>("AdminUsers", userId);
}
public async Task<AdminUser?> GetUserByEmailAsync(string email)
{
return (await _mongoService.GetAllAsync<AdminUser>("AdminUsers")).FirstOrDefault((AdminUser u) => u.Email.ToLower() == email.ToLower());
}
public async Task<AdminUser> CreateUserAsync(string email, string password, string name, string role = "Admin")
{
AdminUser user = new AdminUser
{
Email = email,
PasswordHash = HashPassword(password),
Name = name,
Role = role,
IsActive = true,
CreatedAt = DateTime.UtcNow
};
await _mongoService.InsertAsync("AdminUsers", user);
return user;
}
public async Task<bool> ChangePasswordAsync(string userId, string oldPassword, string newPassword)
{
AdminUser adminUser = await GetUserByIdAsync(userId);
if (adminUser == null || !VerifyPassword(oldPassword, adminUser.PasswordHash))
{
return false;
}
adminUser.PasswordHash = HashPassword(newPassword);
await _mongoService.UpdateAsync("AdminUsers", userId, adminUser);
return true;
}
public async Task<bool> ResetPasswordAsync(string userId, string newPassword)
{
AdminUser adminUser = await GetUserByIdAsync(userId);
if (adminUser == null)
{
return false;
}
adminUser.PasswordHash = HashPassword(newPassword);
await _mongoService.UpdateAsync("AdminUsers", userId, adminUser);
return true;
}
}

View File

@@ -1,67 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
namespace SkyArtShop.Services;
public class MongoDBService
{
private readonly IMongoDatabase _database;
private readonly MongoDBSettings _settings;
public MongoDBService(IOptions<MongoDBSettings> settings)
{
_settings = settings.Value;
MongoClientSettings mongoClientSettings = MongoClientSettings.FromConnectionString(_settings.ConnectionString);
mongoClientSettings.MaxConnectionPoolSize = 500;
mongoClientSettings.MinConnectionPoolSize = 50;
mongoClientSettings.WaitQueueTimeout = TimeSpan.FromSeconds(30.0);
mongoClientSettings.ServerSelectionTimeout = TimeSpan.FromSeconds(10.0);
mongoClientSettings.ConnectTimeout = TimeSpan.FromSeconds(10.0);
mongoClientSettings.SocketTimeout = TimeSpan.FromSeconds(60.0);
MongoClient mongoClient = new MongoClient(mongoClientSettings);
_database = mongoClient.GetDatabase(_settings.DatabaseName);
}
public IMongoCollection<T> GetCollection<T>(string collectionName)
{
return _database.GetCollection<T>(collectionName);
}
public async Task<List<T>> GetAllAsync<T>(string collectionName)
{
IMongoCollection<T> collection = GetCollection<T>(collectionName);
return await collection.Find((T _) => true).ToListAsync();
}
public async Task<T> GetByIdAsync<T>(string collectionName, string id)
{
IMongoCollection<T> collection = GetCollection<T>(collectionName);
FilterDefinition<T> filter = Builders<T>.Filter.Eq("_id", ObjectId.Parse(id));
return await collection.Find(filter).FirstOrDefaultAsync();
}
public async Task InsertAsync<T>(string collectionName, T document)
{
IMongoCollection<T> collection = GetCollection<T>(collectionName);
await collection.InsertOneAsync(document);
}
public async Task UpdateAsync<T>(string collectionName, string id, T document)
{
IMongoCollection<T> collection = GetCollection<T>(collectionName);
FilterDefinition<T> filter = Builders<T>.Filter.Eq("_id", ObjectId.Parse(id));
await collection.ReplaceOneAsync(filter, document);
}
public async Task DeleteAsync<T>(string collectionName, string id)
{
IMongoCollection<T> collection = GetCollection<T>(collectionName);
FilterDefinition<T> filter = Builders<T>.Filter.Eq("_id", ObjectId.Parse(id));
await collection.DeleteOneAsync(filter);
}
}

View File

@@ -1,12 +0,0 @@
using System.Collections.Generic;
namespace SkyArtShop.Services;
public class MongoDBSettings
{
public string ConnectionString { get; set; } = string.Empty;
public string DatabaseName { get; set; } = string.Empty;
public Dictionary<string, string> Collections { get; set; } = new Dictionary<string, string>();
}

View File

@@ -1,120 +0,0 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using SkyArtShop.Models;
namespace SkyArtShop.Services;
public class PostgreAuthService
{
private readonly PostgreSQLService _pgService;
public PostgreAuthService(PostgreSQLService pgService)
{
_pgService = pgService;
}
public string HashPassword(string password)
{
using RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create();
byte[] array = new byte[16];
randomNumberGenerator.GetBytes(array);
using Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, array, 10000, HashAlgorithmName.SHA256);
byte[] bytes = rfc2898DeriveBytes.GetBytes(32);
byte[] array2 = new byte[48];
Array.Copy(array, 0, array2, 0, 16);
Array.Copy(bytes, 0, array2, 16, 32);
return Convert.ToBase64String(array2);
}
public bool VerifyPassword(string password, string hashedPassword)
{
try
{
if (hashedPassword.Contains(':'))
{
string[] array = hashedPassword.Split(':');
if (array.Length != 3)
{
return false;
}
int iterations = int.Parse(array[0]);
byte[] salt = Convert.FromBase64String(array[1]);
byte[] array2 = Convert.FromBase64String(array[2]);
byte[] array3 = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterations, HashAlgorithmName.SHA256, array2.Length);
return CryptographicOperations.FixedTimeEquals(array2, array3);
}
byte[] array4 = Convert.FromBase64String(hashedPassword);
byte[] array5 = new byte[16];
Array.Copy(array4, 0, array5, 0, 16);
using Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, array5, 10000, HashAlgorithmName.SHA256);
byte[] bytes = rfc2898DeriveBytes.GetBytes(32);
for (int i = 0; i < 32; i++)
{
if (array4[i + 16] != bytes[i])
{
return false;
}
}
return true;
}
catch
{
return false;
}
}
public async Task<AdminUser?> AuthenticateAsync(string email, string password)
{
AdminUser user = await _pgService.GetUserByEmailAsync(email);
if (user == null || !VerifyPassword(password, user.PasswordHash))
{
return null;
}
await _pgService.UpdateUserLastLoginAsync(user.Id, DateTime.UtcNow);
user.LastLogin = DateTime.UtcNow;
return user;
}
public ClaimsPrincipal CreateClaimsPrincipal(AdminUser user)
{
List<Claim> list = new List<Claim>
{
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", user.Id),
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", user.Email),
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", user.Name),
new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", user.Role)
};
foreach (string permission in user.Permissions)
{
list.Add(new Claim("Permission", permission));
}
ClaimsIdentity identity = new ClaimsIdentity(list, "Cookies");
return new ClaimsPrincipal(identity);
}
public async Task<AdminUser?> GetUserByEmailAsync(string email)
{
return await _pgService.GetUserByEmailAsync(email);
}
public async Task<AdminUser> CreateUserAsync(string email, string password, string name, string role = "Admin")
{
AdminUser user = new AdminUser
{
Id = Guid.NewGuid().ToString(),
Email = email,
PasswordHash = HashPassword(password),
Name = name,
Role = role,
IsActive = true,
CreatedAt = DateTime.UtcNow,
Permissions = new List<string>()
};
await _pgService.CreateAdminUserAsync(user);
return user;
}
}

View File

@@ -1,466 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
using Npgsql;
using NpgsqlTypes;
using SkyArtShop.Models;
namespace SkyArtShop.Services;
public class PostgreSQLService
{
private readonly string _connectionString;
public PostgreSQLService(string connectionString)
{
_connectionString = connectionString;
}
private async Task<NpgsqlConnection> GetConnectionAsync()
{
NpgsqlConnection conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync();
return conn;
}
public async Task<List<T>> GetAllAsync<T>(string tableName) where T : class, new()
{
using NpgsqlConnection conn = await GetConnectionAsync();
string cmdText = "SELECT * FROM " + tableName.ToLower();
using NpgsqlCommand cmd = new NpgsqlCommand(cmdText, conn);
using NpgsqlDataReader reader = await cmd.ExecuteReaderAsync();
List<T> results = new List<T>();
while (await reader.ReadAsync())
{
results.Add(MapToObject<T>(reader));
}
return results;
}
public async Task<T?> GetByIdAsync<T>(string tableName, string id) where T : class, new()
{
using NpgsqlConnection conn = await GetConnectionAsync();
string cmdText = "SELECT * FROM " + tableName.ToLower() + " WHERE id = @id";
using NpgsqlCommand cmd = new NpgsqlCommand(cmdText, conn);
cmd.Parameters.AddWithValue("id", id);
using NpgsqlDataReader reader = await cmd.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return MapToObject<T>(reader);
}
return null;
}
public async Task<AdminUser?> GetUserByEmailAsync(string email)
{
using NpgsqlConnection conn = await GetConnectionAsync();
string cmdText = "SELECT * FROM adminusers WHERE LOWER(email) = LOWER(@email) AND isactive = true";
using NpgsqlCommand cmd = new NpgsqlCommand(cmdText, conn);
cmd.Parameters.AddWithValue("email", email);
using NpgsqlDataReader reader = await cmd.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return MapToAdminUser(reader);
}
return null;
}
public async Task UpdateUserLastLoginAsync(string userId, DateTime lastLogin)
{
using NpgsqlConnection conn = await GetConnectionAsync();
string cmdText = "UPDATE adminusers SET lastlogin = @lastlogin WHERE id = @id";
using NpgsqlCommand cmd = new NpgsqlCommand(cmdText, conn);
cmd.Parameters.AddWithValue("id", userId);
cmd.Parameters.AddWithValue("lastlogin", lastLogin);
await cmd.ExecuteNonQueryAsync();
}
public async Task CreateAdminUserAsync(AdminUser user)
{
using NpgsqlConnection conn = await GetConnectionAsync();
string cmdText = "INSERT INTO adminusers (id, email, passwordhash, name, role, permissions, isactive, createdby, createdat)\n VALUES (@id, @email, @passwordhash, @name, @role, @permissions::jsonb, @isactive, @createdby, @createdat)";
using NpgsqlCommand cmd = new NpgsqlCommand(cmdText, conn);
cmd.Parameters.AddWithValue("id", Guid.NewGuid().ToString());
cmd.Parameters.AddWithValue("email", user.Email);
cmd.Parameters.AddWithValue("passwordhash", user.PasswordHash);
cmd.Parameters.AddWithValue("name", user.Name);
cmd.Parameters.AddWithValue("role", user.Role);
cmd.Parameters.AddWithValue("permissions", JsonSerializer.Serialize(user.Permissions));
cmd.Parameters.AddWithValue("isactive", user.IsActive);
cmd.Parameters.AddWithValue("createdby", user.CreatedBy);
cmd.Parameters.AddWithValue("createdat", user.CreatedAt);
await cmd.ExecuteNonQueryAsync();
}
public async Task<List<Product>> GetProductsAsync()
{
using NpgsqlConnection conn = await GetConnectionAsync();
string cmdText = "SELECT * FROM products WHERE isactive = true ORDER BY createdat DESC";
using NpgsqlCommand cmd = new NpgsqlCommand(cmdText, conn);
using NpgsqlDataReader reader = await cmd.ExecuteReaderAsync();
List<Product> results = new List<Product>();
while (await reader.ReadAsync())
{
results.Add(MapToProduct(reader));
}
return results;
}
public async Task<Product?> GetProductBySlugAsync(string slug)
{
using NpgsqlConnection conn = await GetConnectionAsync();
string cmdText = "SELECT * FROM products WHERE slug = @slug AND isactive = true";
using NpgsqlCommand cmd = new NpgsqlCommand(cmdText, conn);
cmd.Parameters.AddWithValue("slug", slug);
using NpgsqlDataReader reader = await cmd.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return MapToProduct(reader);
}
return null;
}
public async Task<Page?> GetPageBySlugAsync(string slug)
{
using NpgsqlConnection conn = await GetConnectionAsync();
string cmdText = "SELECT * FROM pages WHERE pageslug = @slug AND isactive = true";
using NpgsqlCommand cmd = new NpgsqlCommand(cmdText, conn);
cmd.Parameters.AddWithValue("slug", slug);
using NpgsqlDataReader reader = await cmd.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return MapToPage(reader);
}
return null;
}
public async Task<SiteSettings?> GetSiteSettingsAsync()
{
using NpgsqlConnection conn = await GetConnectionAsync();
string cmdText = "SELECT * FROM sitesettings LIMIT 1";
using NpgsqlCommand cmd = new NpgsqlCommand(cmdText, conn);
using NpgsqlDataReader reader = await cmd.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return MapToSiteSettings(reader);
}
return null;
}
public async Task<List<MenuItem>> GetMenuItemsAsync()
{
using NpgsqlConnection conn = await GetConnectionAsync();
string cmdText = "SELECT * FROM menuitems WHERE isactive = true ORDER BY displayorder";
using NpgsqlCommand cmd = new NpgsqlCommand(cmdText, conn);
using NpgsqlDataReader reader = await cmd.ExecuteReaderAsync();
List<MenuItem> results = new List<MenuItem>();
while (await reader.ReadAsync())
{
results.Add(MapToMenuItem(reader));
}
return results;
}
public async Task InsertAsync<T>(string tableName, T entity) where T : class
{
using NpgsqlConnection conn = await GetConnectionAsync();
Type typeFromHandle = typeof(T);
List<PropertyInfo> list = (from p in typeFromHandle.GetProperties()
where p.CanRead && p.Name != "Id"
select p).ToList();
PropertyInfo property = typeFromHandle.GetProperty("Id");
if (property != null && string.IsNullOrEmpty(property.GetValue(entity)?.ToString()))
{
property.SetValue(entity, Guid.NewGuid().ToString());
}
string value = string.Join(", ", list.Select((PropertyInfo p) => p.Name.ToLower()).Prepend("id"));
string value2 = string.Join(", ", list.Select((PropertyInfo p) => "@" + p.Name.ToLower()).Prepend("@id"));
string cmdText = $"INSERT INTO {tableName.ToLower()} ({value}) VALUES ({value2})";
using NpgsqlCommand cmd = new NpgsqlCommand(cmdText, conn);
cmd.Parameters.AddWithValue("id", property?.GetValue(entity)?.ToString() ?? Guid.NewGuid().ToString());
foreach (PropertyInfo item in list)
{
object value3 = item.GetValue(entity);
string parameterName = item.Name.ToLower();
if (value3 == null)
{
cmd.Parameters.AddWithValue(parameterName, DBNull.Value);
}
else if (item.PropertyType == typeof(List<string>) || item.PropertyType == typeof(List<ProductVariant>) || item.PropertyType == typeof(List<TeamMember>) || item.PropertyType == typeof(Dictionary<string, string>))
{
cmd.Parameters.AddWithValue(parameterName, NpgsqlDbType.Jsonb, JsonSerializer.Serialize(value3));
}
else if (item.PropertyType == typeof(DateTime) || item.PropertyType == typeof(DateTime?))
{
cmd.Parameters.AddWithValue(parameterName, value3);
}
else if (item.PropertyType == typeof(bool) || item.PropertyType == typeof(bool?))
{
cmd.Parameters.AddWithValue(parameterName, value3);
}
else
{
cmd.Parameters.AddWithValue(parameterName, value3);
}
}
await cmd.ExecuteNonQueryAsync();
}
public async Task UpdateAsync<T>(string tableName, string id, T entity) where T : class
{
using NpgsqlConnection conn = await GetConnectionAsync();
Type typeFromHandle = typeof(T);
List<PropertyInfo> list = (from p in typeFromHandle.GetProperties()
where p.CanRead && p.Name != "Id"
select p).ToList();
string value = string.Join(", ", list.Select((PropertyInfo p) => p.Name.ToLower() + " = @" + p.Name.ToLower()));
string cmdText = $"UPDATE {tableName.ToLower()} SET {value} WHERE id = @id";
using NpgsqlCommand cmd = new NpgsqlCommand(cmdText, conn);
cmd.Parameters.AddWithValue("id", id);
foreach (PropertyInfo item in list)
{
object value2 = item.GetValue(entity);
string parameterName = item.Name.ToLower();
if (value2 == null)
{
cmd.Parameters.AddWithValue(parameterName, DBNull.Value);
}
else if (item.PropertyType == typeof(List<string>) || item.PropertyType == typeof(List<ProductVariant>) || item.PropertyType == typeof(List<TeamMember>) || item.PropertyType == typeof(Dictionary<string, string>))
{
cmd.Parameters.AddWithValue(parameterName, NpgsqlDbType.Jsonb, JsonSerializer.Serialize(value2));
}
else if (item.PropertyType == typeof(DateTime) || item.PropertyType == typeof(DateTime?))
{
cmd.Parameters.AddWithValue(parameterName, value2);
}
else if (item.PropertyType == typeof(bool) || item.PropertyType == typeof(bool?))
{
cmd.Parameters.AddWithValue(parameterName, value2);
}
else
{
cmd.Parameters.AddWithValue(parameterName, value2);
}
}
await cmd.ExecuteNonQueryAsync();
}
public async Task DeleteAsync<T>(string tableName, string id) where T : class
{
using NpgsqlConnection conn = await GetConnectionAsync();
string cmdText = "DELETE FROM " + tableName.ToLower() + " WHERE id = @id";
using NpgsqlCommand cmd = new NpgsqlCommand(cmdText, conn);
cmd.Parameters.AddWithValue("id", id);
await cmd.ExecuteNonQueryAsync();
}
private T MapToObject<T>(NpgsqlDataReader reader) where T : class, new()
{
Type typeFromHandle = typeof(T);
if (typeFromHandle == typeof(AdminUser))
{
return (MapToAdminUser(reader) as T) ?? new T();
}
if (typeFromHandle == typeof(Product))
{
return (MapToProduct(reader) as T) ?? new T();
}
if (typeFromHandle == typeof(Page))
{
return (MapToPage(reader) as T) ?? new T();
}
if (typeFromHandle == typeof(MenuItem))
{
return (MapToMenuItem(reader) as T) ?? new T();
}
if (typeFromHandle == typeof(SiteSettings))
{
return (MapToSiteSettings(reader) as T) ?? new T();
}
if (typeFromHandle == typeof(PortfolioCategory))
{
return (MapToPortfolioCategory(reader) as T) ?? new T();
}
if (typeFromHandle == typeof(PortfolioProject))
{
return (MapToPortfolioProject(reader) as T) ?? new T();
}
if (typeFromHandle == typeof(BlogPost))
{
return (MapToBlogPost(reader) as T) ?? new T();
}
if (typeFromHandle == typeof(HomepageSection))
{
return (MapToHomepageSection(reader) as T) ?? new T();
}
return new T();
}
private AdminUser MapToAdminUser(NpgsqlDataReader reader)
{
AdminUser adminUser = new AdminUser();
adminUser.Id = reader["id"].ToString();
adminUser.Email = reader["email"].ToString() ?? "";
adminUser.PasswordHash = reader["passwordhash"].ToString() ?? "";
adminUser.Name = reader["name"].ToString() ?? "";
adminUser.Role = reader["role"].ToString() ?? "Admin";
adminUser.Permissions = JsonSerializer.Deserialize<List<string>>(reader["permissions"].ToString() ?? "[]") ?? new List<string>();
adminUser.IsActive = (reader["isactive"] as bool?) ?? true;
adminUser.CreatedBy = reader["createdby"]?.ToString() ?? "";
adminUser.CreatedAt = (reader["createdat"] as DateTime?) ?? DateTime.UtcNow;
adminUser.LastLogin = (reader.IsDBNull(reader.GetOrdinal("lastlogin")) ? ((DateTime?)null) : new DateTime?(reader.GetDateTime(reader.GetOrdinal("lastlogin"))));
adminUser.Phone = reader["phone"]?.ToString() ?? "";
adminUser.Notes = reader["notes"]?.ToString() ?? "";
return adminUser;
}
private Product MapToProduct(NpgsqlDataReader reader)
{
Product product = new Product();
product.Id = reader["id"].ToString();
product.Name = reader["name"].ToString() ?? "";
product.Slug = reader["slug"].ToString() ?? "";
product.SKU = reader["sku"]?.ToString() ?? "";
product.ShortDescription = reader["shortdescription"]?.ToString() ?? "";
product.Description = reader["description"]?.ToString() ?? "";
product.Price = (reader["price"] as decimal?).GetValueOrDefault();
product.Category = reader["category"]?.ToString() ?? "";
product.Color = reader["color"]?.ToString() ?? "";
product.Colors = JsonSerializer.Deserialize<List<string>>(reader["colors"].ToString() ?? "[]") ?? new List<string>();
product.ImageUrl = reader["imageurl"]?.ToString() ?? "";
product.Images = JsonSerializer.Deserialize<List<string>>(reader["images"].ToString() ?? "[]") ?? new List<string>();
product.IsFeatured = reader["isfeatured"] as bool? == true;
product.IsTopSeller = reader["istopseller"] as bool? == true;
product.StockQuantity = (reader["stockquantity"] as int?).GetValueOrDefault();
product.IsActive = (reader["isactive"] as bool?) ?? true;
product.UnitsSold = (reader["unitssold"] as int?).GetValueOrDefault();
product.TotalRevenue = (reader["totalrevenue"] as decimal?).GetValueOrDefault();
product.AverageRating = (reader["averagerating"] as double?).GetValueOrDefault();
product.TotalReviews = (reader["totalreviews"] as int?).GetValueOrDefault();
product.CostPrice = (reader["costprice"] as decimal?).GetValueOrDefault();
product.Tags = JsonSerializer.Deserialize<List<string>>(reader["tags"].ToString() ?? "[]") ?? new List<string>();
product.MetaDescription = reader["metadescription"]?.ToString() ?? "";
product.CreatedAt = (reader["createdat"] as DateTime?) ?? DateTime.UtcNow;
product.UpdatedAt = (reader["updatedat"] as DateTime?) ?? DateTime.UtcNow;
return product;
}
private Page MapToPage(NpgsqlDataReader reader)
{
Page page = new Page();
page.Id = reader["id"].ToString();
page.PageName = reader["pagename"].ToString() ?? "";
page.PageSlug = reader["pageslug"].ToString() ?? "";
page.Title = reader["title"]?.ToString() ?? "";
page.Subtitle = reader["subtitle"]?.ToString() ?? "";
page.HeroImage = reader["heroimage"]?.ToString() ?? "";
page.Content = reader["content"]?.ToString() ?? "";
page.MetaDescription = reader["metadescription"]?.ToString() ?? "";
page.ImageGallery = JsonSerializer.Deserialize<List<string>>(reader["imagegallery"].ToString() ?? "[]") ?? new List<string>();
page.IsActive = (reader["isactive"] as bool?) ?? true;
page.CreatedAt = (reader["createdat"] as DateTime?) ?? DateTime.UtcNow;
page.UpdatedAt = (reader["updatedat"] as DateTime?) ?? DateTime.UtcNow;
return page;
}
private MenuItem MapToMenuItem(NpgsqlDataReader reader)
{
MenuItem menuItem = new MenuItem();
menuItem.Id = reader["id"].ToString();
menuItem.Label = reader["label"].ToString() ?? "";
menuItem.Url = reader["url"].ToString() ?? "";
menuItem.DisplayOrder = (reader["displayorder"] as int?).GetValueOrDefault();
menuItem.IsActive = (reader["isactive"] as bool?) ?? true;
menuItem.ShowInNavbar = (reader["showinnavbar"] as bool?) ?? true;
menuItem.ShowInDropdown = (reader["showindropdown"] as bool?) ?? true;
menuItem.OpenInNewTab = reader["openinnewtab"] as bool? == true;
menuItem.CreatedAt = (reader["createdat"] as DateTime?) ?? DateTime.UtcNow;
return menuItem;
}
private SiteSettings MapToSiteSettings(NpgsqlDataReader reader)
{
SiteSettings siteSettings = new SiteSettings();
siteSettings.Id = reader["id"].ToString();
siteSettings.SiteName = reader["sitename"]?.ToString() ?? "Sky Art Shop";
siteSettings.SiteTagline = reader["sitetagline"]?.ToString() ?? "";
siteSettings.ContactEmail = reader["contactemail"]?.ToString() ?? "";
siteSettings.ContactPhone = reader["contactphone"]?.ToString() ?? "";
siteSettings.InstagramUrl = reader["instagramurl"]?.ToString() ?? "#";
siteSettings.FooterText = reader["footertext"]?.ToString() ?? "";
siteSettings.UpdatedAt = (reader["updatedat"] as DateTime?) ?? DateTime.UtcNow;
return siteSettings;
}
private PortfolioCategory MapToPortfolioCategory(NpgsqlDataReader reader)
{
PortfolioCategory portfolioCategory = new PortfolioCategory();
portfolioCategory.Id = reader["id"].ToString();
portfolioCategory.Name = reader["name"].ToString() ?? "";
portfolioCategory.Slug = reader["slug"].ToString() ?? "";
portfolioCategory.Description = reader["description"]?.ToString() ?? "";
portfolioCategory.ThumbnailImage = reader["thumbnailimage"]?.ToString() ?? "";
portfolioCategory.FeaturedImage = reader["featuredimage"]?.ToString() ?? "";
portfolioCategory.DisplayOrder = (reader["displayorder"] as int?).GetValueOrDefault();
portfolioCategory.IsActive = (reader["isactive"] as bool?) ?? true;
portfolioCategory.CreatedAt = (reader["createdat"] as DateTime?) ?? DateTime.UtcNow;
portfolioCategory.UpdatedAt = (reader["updatedat"] as DateTime?) ?? DateTime.UtcNow;
return portfolioCategory;
}
private BlogPost MapToBlogPost(NpgsqlDataReader reader)
{
BlogPost blogPost = new BlogPost();
blogPost.Id = reader["id"].ToString();
blogPost.Title = reader["title"].ToString() ?? "";
blogPost.Slug = reader["slug"].ToString() ?? "";
blogPost.Content = reader["content"]?.ToString() ?? "";
blogPost.Excerpt = reader["excerpt"]?.ToString() ?? "";
blogPost.FeaturedImage = reader["featuredimage"]?.ToString() ?? "";
blogPost.Author = reader["author"]?.ToString() ?? "";
blogPost.Tags = JsonSerializer.Deserialize<List<string>>(reader["tags"].ToString() ?? "[]") ?? new List<string>();
blogPost.IsPublished = (reader["ispublished"] as bool?) ?? true;
blogPost.PublishedDate = (reader["publisheddate"] as DateTime?) ?? DateTime.UtcNow;
blogPost.CreatedAt = (reader["createdat"] as DateTime?) ?? DateTime.UtcNow;
blogPost.UpdatedAt = (reader["updatedat"] as DateTime?) ?? DateTime.UtcNow;
return blogPost;
}
private HomepageSection MapToHomepageSection(NpgsqlDataReader reader)
{
HomepageSection homepageSection = new HomepageSection();
homepageSection.Id = reader["id"].ToString();
homepageSection.Title = reader["title"]?.ToString() ?? "";
homepageSection.Subtitle = reader["subtitle"]?.ToString() ?? "";
homepageSection.Content = reader["content"]?.ToString() ?? "";
homepageSection.SectionType = reader["sectiontype"]?.ToString() ?? "";
homepageSection.ImageUrl = reader["imageurl"]?.ToString() ?? "";
homepageSection.ButtonText = reader["buttontext"]?.ToString() ?? "";
homepageSection.ButtonUrl = reader["buttonurl"]?.ToString() ?? "";
homepageSection.IsActive = (reader["isactive"] as bool?) ?? true;
homepageSection.DisplayOrder = (reader["displayorder"] as int?).GetValueOrDefault();
homepageSection.AdditionalData = JsonSerializer.Deserialize<Dictionary<string, string>>(reader["additionaldata"]?.ToString() ?? "{}") ?? new Dictionary<string, string>();
homepageSection.CreatedAt = (reader["createdat"] as DateTime?) ?? DateTime.UtcNow;
homepageSection.UpdatedAt = (reader["updatedat"] as DateTime?) ?? DateTime.UtcNow;
return homepageSection;
}
private PortfolioProject MapToPortfolioProject(NpgsqlDataReader reader)
{
PortfolioProject portfolioProject = new PortfolioProject();
portfolioProject.Id = reader["id"].ToString();
portfolioProject.CategoryId = reader["categoryid"]?.ToString() ?? "";
portfolioProject.Title = reader["title"]?.ToString() ?? "";
portfolioProject.Description = reader["description"]?.ToString() ?? "";
portfolioProject.FeaturedImage = reader["featuredimage"]?.ToString() ?? "";
portfolioProject.Images = JsonSerializer.Deserialize<List<string>>(reader["images"].ToString() ?? "[]") ?? new List<string>();
portfolioProject.DisplayOrder = (reader["displayorder"] as int?).GetValueOrDefault();
portfolioProject.IsActive = (reader["isactive"] as bool?) ?? true;
portfolioProject.CreatedAt = (reader["createdat"] as DateTime?) ?? DateTime.UtcNow;
portfolioProject.UpdatedAt = (reader["updatedat"] as DateTime?) ?? DateTime.UtcNow;
return portfolioProject;
}
}

View File

@@ -1,20 +0,0 @@
using System.Text.RegularExpressions;
namespace SkyArtShop.Services;
public class SlugService
{
public string GenerateSlug(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return string.Empty;
}
string text2 = text.ToLowerInvariant();
text2 = text2.Replace(" ", "-");
text2 = text2.Replace("&", "and");
text2 = Regex.Replace(text2, "[^a-z0-9\\-]", "");
text2 = Regex.Replace(text2, "-+", "-");
return text2.Trim('-');
}
}

View File

@@ -1,98 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using SkyArtShop.Data;
using SkyArtShop.Models;
namespace SkyArtShop.Services;
public class SqlDataService
{
private readonly SkyArtShopDbContext _context;
public SqlDataService(SkyArtShopDbContext context)
{
_context = context;
}
public async Task<List<T>> GetAllAsync<T>() where T : class
{
return await _context.Set<T>().ToListAsync();
}
public async Task<T?> GetByIdAsync<T>(string id) where T : class
{
return await _context.Set<T>().FindAsync(id);
}
public async Task<T> InsertAsync<T>(T entity) where T : class
{
_context.Set<T>().Add(entity);
await _context.SaveChangesAsync();
return entity;
}
public async Task<T> UpdateAsync<T>(T entity) where T : class
{
_context.Set<T>().Update(entity);
await _context.SaveChangesAsync();
return entity;
}
public async Task DeleteAsync<T>(string id) where T : class
{
T val = await GetByIdAsync<T>(id);
if (val != null)
{
_context.Set<T>().Remove(val);
await _context.SaveChangesAsync();
}
}
public async Task<AdminUser?> GetUserByEmailAsync(string email)
{
return await _context.AdminUsers.FirstOrDefaultAsync((AdminUser u) => u.Email.ToLower() == email.ToLower());
}
public async Task<List<Product>> GetFeaturedProductsAsync()
{
return await _context.Products.Where((Product p) => p.IsFeatured && p.IsActive).ToListAsync();
}
public async Task<List<Product>> GetTopSellersAsync(int count = 10)
{
return await (from p in _context.Products
where p.IsTopSeller && p.IsActive
orderby p.UnitsSold descending
select p).Take(count).ToListAsync();
}
public async Task<Product?> GetProductBySlugAsync(string slug)
{
return await _context.Products.FirstOrDefaultAsync((Product p) => p.Slug == slug && p.IsActive);
}
public async Task<BlogPost?> GetBlogPostBySlugAsync(string slug)
{
return await _context.BlogPosts.FirstOrDefaultAsync((BlogPost b) => b.Slug == slug && b.IsPublished);
}
public async Task<Page?> GetPageBySlugAsync(string slug)
{
return await _context.Pages.FirstOrDefaultAsync((Page p) => p.PageSlug == slug && p.IsActive);
}
public async Task<SiteSettings?> GetSiteSettingsAsync()
{
return await _context.SiteSettings.FirstOrDefaultAsync();
}
public async Task<List<MenuItem>> GetActiveMenuItemsAsync()
{
return await (from m in _context.MenuItems
where m.IsActive
orderby m.DisplayOrder
select m).ToListAsync();
}
}

View File

@@ -1,71 +0,0 @@
<!-- Sky Art Shop - ASP.NET Core CMS Project -->
# Copilot Instructions for Sky Art Shop
## Project Overview
Dynamic e-commerce CMS built with ASP.NET Core MVC 8.0, MongoDB for content, and ASP.NET Core Identity for authentication.
## Completed Tasks
- [x] ASP.NET Core MVC structure created
- [x] MongoDB integration (Products, Portfolio, Blog, Pages, Settings, MenuItems)
- [x] ASP.NET Core Identity + SQLite for authentication
- [x] Admin panel with CRUD for all content types
- [x] Public pages (Home, Shop, Portfolio, Blog, About, Contact)
- [x] CKEditor 5 rich text editor (no API key required)
- [x] Image upload service (wwwroot/uploads/images)
- [x] Dynamic navigation via ViewComponent
- [x] Seeding for default data (admin user, settings, categories, menus)
- [x] Clean build with zero errors
- [x] Application tested and running on http://localhost:5000
- [x] README documentation updated
## Architecture
- **Backend**: ASP.NET Core 8.0 MVC
- **Content DB**: MongoDB (connection string in appsettings.json)
- **Auth DB**: SQLite + EF Core + Identity
- **Admin Auth**: Role-based (Admin role)
- **Views**: Razor + Bootstrap 5 (admin) + custom CSS (public)
## Key Files
- `Program.cs`: Middleware, services, database initialization
- `Models/DatabaseModels.cs`: MongoDB entity models
- `Services/MongoDBService.cs`: Generic MongoDB CRUD service
- `Data/ApplicationDbContext.cs`: EF Core Identity context
- `Controllers/Admin*.cs`: Admin CRUD controllers ([Authorize(Roles="Admin")])
- `Controllers/*.cs`: Public controllers (Shop, Portfolio, Blog, About, Contact)
- `Views/Shared/_Layout.cshtml`: Public layout with dynamic navigation
- `Views/Shared/_AdminLayout.cshtml`: Admin dashboard layout
- `ViewComponents/NavigationViewComponent.cs`: Dynamic menu rendering
## Running the Project
```powershell
dotnet build # Build solution
dotnet run # Start on http://localhost:5000
```
## Admin Access
- URL: http://localhost:5000/admin/login
- Default: admin@skyartshop.com / Admin123! (configure in appsettings.json)
## Future Development Guidelines
- Use MongoDBService for all MongoDB operations
- Admin controllers must use [Authorize(Roles="Admin")]
- Slug generation: lowercase, replace spaces with hyphens
- TempData["SuccessMessage"] / TempData["ErrorMessage"] for user feedback
- Image uploads go to wwwroot/uploads/images with GUID filenames
- All views use Razor syntax; avoid direct HTML files
## Optional Enhancements
- Server-side validation (DataAnnotations)
- Email service for contact form
- Shopping cart/checkout
- SEO meta tags
- Centralized slug utility service

View File

@@ -1,16 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Build Sky_Art_Shop after nav changes",
"type": "shell",
"command": "dotnet build",
"args": [],
"isBackground": false,
"problemMatcher": [
"$msCompile"
],
"group": "build"
}
]
}

View File

@@ -1,333 +0,0 @@
# 🎨 Sky Art Shop - Complete CMS Integration
## 🎉 What's Been Completed
Your Sky Art Shop now has a **full backend CMS** (Admin) connected to your **static frontend** (shop.html, portfolio.html, blog.html, etc.).
### ✅ Backend (Admin Folder)
- **ASP.NET Core 8.0** MVC application
- **MongoDB** database for all content
- **Authentication** (cookie-based, admin login)
- **CRUD Operations** for:
- Products
- Portfolio Projects
- Blog Posts
- Pages
- Categories
- Site Settings
- **Image Upload** service with validation
- **Public Read-Only APIs** with CORS enabled
- **Static File Serving** for uploaded images
### ✅ Frontend Integration Files Created
Located in `Sky_Art_Shop/` folder:
```
js/
├── api-integration.js # Core API functions
├── shop-page.js # Products integration
├── portfolio-page.js # Projects integration
├── blog-page.js # Blog integration
├── index-page.js # Home page integration
└── about-page.js # About page integration
css/
└── api-styles.css # Styling for cards & grids
Documentation/
├── INTEGRATION_GUIDE.md # How to wire up each page
├── IMAGE_FIX_GUIDE.md # Fix for images not showing
└── test-api.html # Test page to verify API
```
---
## 🚀 Quick Start (3 Steps)
### 1. Start the Backend
```powershell
cd "e:\Documents\Website Projects\Sky_Art_Shop\Admin"
dotnet run --launch-profile https
```
Backend runs on: **<https://localhost:5001>**
### 2. Fix Missing Images
Your products exist but don't have images yet:
1. Open: <https://localhost:5001/admin/products>
2. Click **Edit** on each product
3. Upload a **Main Image**
4. Click **Save**
Images will be stored in `Admin/wwwroot/uploads/products/`
### 3. Integrate Static Pages
Add these scripts to each HTML file (see `INTEGRATION_GUIDE.md` for details):
**shop.html**:
```html
<link rel="stylesheet" href="css/api-styles.css">
<script src="js/api-integration.js"></script>
<script src="js/shop-page.js"></script>
```
**portfolio.html**:
```html
<link rel="stylesheet" href="css/api-styles.css">
<script src="js/api-integration.js"></script>
<script src="js/portfolio-page.js"></script>
```
**blog.html**:
```html
<link rel="stylesheet" href="css/api-styles.css">
<script src="js/api-integration.js"></script>
<script src="js/blog-page.js"></script>
```
Add container divs where content should render:
```html
<div id="productsContainer"></div>
<div id="projectsContainer"></div>
<div id="blogContainer"></div>
```
---
## 🧪 Testing
### Test API Connection
Open: `file:///E:/Documents/Website%20Projects/Sky_Art_Shop/test-api.html`
This page will:
- ✅ Test backend connection
- ✅ Load products/projects/blog from API
- ✅ Show image URLs and verify they load
- ✅ Display JSON responses for debugging
### Test Your Static Site
After integration:
1. Open `shop.html` in browser
2. Products should render with images from API
3. Open DevTools Console (F12)
4. Should see: `"Loaded products: X"`
---
## 📊 How It Works
```
┌─────────────────────┐
│ Static HTML Files │
│ (shop.html, etc.) │
│ │
│ Uses JavaScript to │
│ fetch data ↓ │
└─────────────────────┘
┌─────────────────────┐
│ Public REST API │
│ /api/products │
│ /api/projects │
│ /api/blog │
│ │
│ Returns JSON ↓ │
└─────────────────────┘
┌─────────────────────┐
│ MongoDB Database │
│ SkyArtShopCMS │
│ │
│ Collections: │
│ - Products │
│ - Projects │
│ - BlogPosts │
│ - Pages │
└─────────────────────┘
```
**Admin edits content****MongoDB updates****Static site fetches new data****Users see changes**
---
## 📋 API Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/products` | GET | All products |
| `/api/products/{id}` | GET | Single product |
| `/api/projects` | GET | Portfolio projects |
| `/api/projects/{id}` | GET | Single project |
| `/api/blog` | GET | Blog posts |
| `/api/blog/{id}` | GET | Single post |
| `/api/pages/{slug}` | GET | Page by slug (e.g., "about") |
| `/api/categories` | GET | All categories |
| `/api/settings` | GET | Site settings |
| `/uploads/products/*` | GET | Product images |
| `/uploads/projects/*` | GET | Project images |
| `/uploads/blog/*` | GET | Blog images |
---
## 🎨 Customization
### Change API URL
Edit `js/api-integration.js`:
```javascript
const API_BASE = 'https://your-domain.com'; // Change from localhost
```
### Customize Card Design
Edit `css/api-styles.css` to match your site's look:
- Colors, fonts, spacing
- Grid layouts (columns, gaps)
- Hover effects
### Custom Rendering
Edit `js/api-integration.js` functions like `loadProducts()` to change HTML structure:
```javascript
return `
<div class="my-custom-card">
<img src="${imgSrc}">
<h3>${product.title}</h3>
<button>Buy Now</button>
</div>`;
```
---
## 🛠️ Admin Features
Access at: **<https://localhost:5001/admin>**
Default credentials (change in Admin → Settings):
- Username: `admin`
- Password: `admin123`
### Admin Capabilities
- ✅ Create/Edit/Delete Products
- ✅ Upload product images (main + gallery)
- ✅ Create/Edit/Delete Portfolio Projects
- ✅ Upload project images (cover + multiple)
- ✅ Create/Edit/Delete Blog Posts
- ✅ Upload featured images for blog
- ✅ Rich text editing (TinyMCE)
- ✅ Manage Pages & Categories
- ✅ Site Settings (title, description, etc.)
---
## 🐛 Troubleshooting
### Images Not Showing?
**Problem**: Products render but no images appear.
**Solution**: See `IMAGE_FIX_GUIDE.md`
1. Edit products in admin
2. Re-upload images
3. Verify files in `Admin/wwwroot/uploads/products/`
### "Cannot reach backend" Error?
**Problem**: Static site can't call API.
**Solution**:
1. Ensure backend is running: `dotnet run --launch-profile https`
2. Check `API_BASE` in `api-integration.js` matches (<https://localhost:5001>)
3. CORS is already enabled
### Blank Page?
**Problem**: HTML page loads but no content.
**Solution**:
1. Open DevTools Console (F12)
2. Check for JavaScript errors
3. Ensure container divs exist (`<div id="productsContainer"></div>`)
4. Verify scripts load in correct order (api-integration.js first)
---
## 📚 Documentation Files
| File | Purpose |
|------|---------|
| `INTEGRATION_GUIDE.md` | Step-by-step integration for each page |
| `IMAGE_FIX_GUIDE.md` | How to fix missing images issue |
| `test-api.html` | Test page to verify API & images |
| `CMS_COMPLETE_GUIDE.md` | This file - complete overview |
---
## ✅ Next Steps
1. **[REQUIRED]** Upload images to products via Admin
2. **[REQUIRED]** Add script tags to HTML pages
3. **[OPTIONAL]** Customize styles in `api-styles.css`
4. **[OPTIONAL]** Add more products/projects/blog posts via Admin
5. **[OPTIONAL]** Deploy backend to a real server (Azure, AWS, etc.)
---
## 🎯 Benefits
**Client-Friendly**: Non-technical users can update content without touching code
**Centralized**: All content managed in one admin panel
**Flexible**: Static site can be hosted anywhere (GitHub Pages, Netlify, etc.)
**Modern Stack**: ASP.NET Core + MongoDB + REST API
**Image Management**: Upload, store, and serve images automatically
**Rich Content**: TinyMCE editor for formatted text
---
## 🚀 Production Deployment (Future)
To deploy to production:
1. **Backend**: Host Admin on Azure, AWS, or VPS
2. **Database**: Use MongoDB Atlas (cloud) or self-hosted
3. **Update API_BASE**: Change localhost to your domain
4. **CORS**: Update policy to allow your domain only
5. **HTTPS**: Use Let's Encrypt or cloud provider SSL
6. **Frontend**: Host static files on CDN/GitHub Pages
---
## 📞 Support
If you encounter issues:
1. Check `test-api.html` to diagnose the problem
2. Review browser DevTools Console (F12)
3. Check backend logs in terminal
4. Refer to troubleshooting guides
---
**Congratulations!** 🎉 Your Sky Art Shop is now a fully functional CMS-powered website. Your client can edit everything via the Admin panel, and changes will appear instantly on the static site.

View File

@@ -1,61 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
public class AboutController : Controller
{
private readonly MongoDBService _mongoService;
private readonly string _pagesCollection = "Pages";
public AboutController(MongoDBService mongoService)
{
_mongoService = mongoService;
}
public async Task<IActionResult> Index()
{
var pages = await _mongoService.GetAllAsync<Page>(_pagesCollection);
var aboutPage = pages.FirstOrDefault(p => p.PageSlug == "about" && p.IsActive);
Console.WriteLine($"[ABOUT] Found About page: {aboutPage != null}");
if (aboutPage != null)
{
Console.WriteLine($"[ABOUT] Title: {aboutPage.Title}");
Console.WriteLine($"[ABOUT] Content length: {aboutPage.Content?.Length ?? 0}");
Console.WriteLine($"[ABOUT] Image Gallery Count: {aboutPage.ImageGallery?.Count ?? 0}");
Console.WriteLine($"[ABOUT] Team Members Count: {aboutPage.TeamMembers?.Count ?? 0}");
if (aboutPage.ImageGallery != null && aboutPage.ImageGallery.Any())
{
Console.WriteLine($"[ABOUT] Gallery Images: {string.Join(", ", aboutPage.ImageGallery)}");
}
if (aboutPage.TeamMembers != null && aboutPage.TeamMembers.Any())
{
foreach (var member in aboutPage.TeamMembers)
{
Console.WriteLine($"[ABOUT] Team: {member.Name} ({member.Role}) - Photo: {member.PhotoUrl}");
}
}
}
if (aboutPage == null)
{
aboutPage = new Page
{
PageName = "About",
PageSlug = "about",
Title = "About Sky Art Shop",
Subtitle = "Creating moments, one craft at a time",
Content = "<h2>Our Story</h2><p>Sky Art Shop specializes in scrapbooking, journaling, cardmaking, and collaging stationery.</p>",
ImageGallery = new List<string>(),
TeamMembers = new List<TeamMember>()
};
}
return View(aboutPage);
}
}
}

View File

@@ -1,86 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
[Route("admin/blog")]
[Authorize(Roles="Admin")]
public class AdminBlogController : Controller
{
private readonly MongoDBService _mongoService;
private readonly SlugService _slugService;
private readonly string _blogCollection = "BlogPosts";
public AdminBlogController(MongoDBService mongoService, SlugService slugService)
{
_mongoService = mongoService;
_slugService = slugService;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
var posts = await _mongoService.GetAllAsync<BlogPost>(_blogCollection);
return View(posts.OrderByDescending(p => p.CreatedAt).ToList());
}
[HttpGet("create")]
public IActionResult Create() => View(new BlogPost());
[HttpPost("create")]
public async Task<IActionResult> Create(BlogPost post)
{
if (!ModelState.IsValid)
{
return View(post);
}
post.CreatedAt = DateTime.UtcNow;
post.UpdatedAt = DateTime.UtcNow;
post.PublishedDate = DateTime.UtcNow;
post.Slug = _slugService.GenerateSlug(post.Title);
await _mongoService.InsertAsync(_blogCollection, post);
TempData["SuccessMessage"] = "Blog post created successfully!";
return RedirectToAction("Index");
}
[HttpGet("edit/{id}")]
public async Task<IActionResult> Edit(string id)
{
var post = await _mongoService.GetByIdAsync<BlogPost>(_blogCollection, id);
if (post == null) return NotFound();
return View(post);
}
[HttpPost("edit/{id}")]
public async Task<IActionResult> Edit(string id, BlogPost post)
{
if (!ModelState.IsValid)
{
return View(post);
}
post.Id = id;
post.UpdatedAt = DateTime.UtcNow;
post.Slug = _slugService.GenerateSlug(post.Title);
await _mongoService.UpdateAsync(_blogCollection, id, post);
TempData["SuccessMessage"] = "Blog post updated successfully!";
return RedirectToAction("Index");
}
[HttpPost("delete/{id}")]
public async Task<IActionResult> Delete(string id)
{
await _mongoService.DeleteAsync<BlogPost>(_blogCollection, id);
TempData["SuccessMessage"] = "Blog post deleted successfully!";
return RedirectToAction("Index");
}
private string GenerateSlug(string text)
{
return _slugService.GenerateSlug(text);
}
}
}

View File

@@ -1,85 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
[Route("admin")]
[Authorize(Roles = "Admin")]
public class AdminController : Controller
{
private readonly MongoDBService _mongoService;
private readonly SignInManager<SkyArtShop.Data.ApplicationUser> _signInManager;
private readonly UserManager<SkyArtShop.Data.ApplicationUser> _userManager;
public AdminController(MongoDBService mongoService,
SignInManager<SkyArtShop.Data.ApplicationUser> signInManager,
UserManager<SkyArtShop.Data.ApplicationUser> userManager)
{
_mongoService = mongoService;
_signInManager = signInManager;
_userManager = userManager;
}
[HttpGet("login")]
[AllowAnonymous]
public IActionResult Login()
{
if (User.Identity?.IsAuthenticated == true)
{
return RedirectToAction("Dashboard");
}
return View();
}
[HttpPost("login")]
[AllowAnonymous]
public async Task<IActionResult> Login(string email, string password)
{
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
ViewBag.Error = "Invalid email or password";
return View();
}
var result = await _signInManager.PasswordSignInAsync(user, password, true, false);
if (!result.Succeeded)
{
ViewBag.Error = "Invalid email or password";
return View();
}
return RedirectToAction("Dashboard");
}
[HttpGet("logout")]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
return RedirectToAction("Login");
}
[HttpGet("dashboard")]
public async Task<IActionResult> Dashboard()
{
var products = await _mongoService.GetAllAsync<Product>("Products");
var projects = await _mongoService.GetAllAsync<PortfolioProject>("PortfolioProjects");
var blogPosts = await _mongoService.GetAllAsync<BlogPost>("BlogPosts");
var pages = await _mongoService.GetAllAsync<Page>("Pages");
var settings = (await _mongoService.GetAllAsync<SiteSettings>("SiteSettings")).FirstOrDefault();
ViewBag.ProductCount = products.Count;
ViewBag.ProjectCount = projects.Count;
ViewBag.BlogCount = blogPosts.Count;
ViewBag.PageCount = pages.Count;
ViewBag.SiteName = settings?.SiteName ?? "Sky Art Shop";
ViewBag.AdminEmail = User.Identity?.Name;
return View();
}
[HttpGet("")]
public IActionResult Index() => RedirectToAction("Dashboard");
}
}

View File

@@ -1,225 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using SkyArtShop.Models;
using SkyArtShop.Services;
using System.Text.Json;
namespace SkyArtShop.Controllers
{
[Route("admin/homepage")]
[Authorize(Roles = "Admin")]
public class AdminHomepageController : Controller
{
private readonly MongoDBService _mongoService;
private readonly IWebHostEnvironment _environment;
private readonly string _sectionsCollection = "HomepageSections";
private readonly string _settingsCollection = "SiteSettings";
public AdminHomepageController(MongoDBService mongoService, IWebHostEnvironment environment)
{
_mongoService = mongoService;
_environment = environment;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
var sections = await _mongoService.GetAllAsync<HomepageSection>(_sectionsCollection);
sections = sections.OrderBy(s => s.DisplayOrder).ToList();
var settingsList = await _mongoService.GetAllAsync<SiteSettings>(_settingsCollection);
var settings = settingsList.FirstOrDefault() ?? new SiteSettings();
ViewBag.Settings = settings;
return View(sections);
}
[HttpGet("section/{id}")]
public async Task<IActionResult> EditSection(string id)
{
var section = await _mongoService.GetByIdAsync<HomepageSection>(_sectionsCollection, id);
if (section == null)
{
TempData["ErrorMessage"] = "Section not found.";
return RedirectToAction("Index");
}
return View(section);
}
[HttpPost("section/update")]
public async Task<IActionResult> UpdateSection(HomepageSection section, IFormFile? imageFile)
{
// Remove Content validation error since it's optional for some section types
ModelState.Remove("Content");
ModelState.Remove("AdditionalData");
if (!ModelState.IsValid)
{
foreach (var error in ModelState.Values.SelectMany(v => v.Errors))
{
Console.WriteLine($"ModelState Error: {error.ErrorMessage}");
}
return View("EditSection", section);
}
Console.WriteLine($"Updating section with ID: {section.Id}");
Console.WriteLine($"Title: {section.Title}");
Console.WriteLine($"Subtitle: {section.Subtitle}");
Console.WriteLine($"Content length: {section.Content?.Length ?? 0}");
Console.WriteLine($"IsActive: {section.IsActive}");
// Get existing section to preserve data
var existingSection = await _mongoService.GetByIdAsync<HomepageSection>(_sectionsCollection, section.Id!);
if (existingSection == null)
{
Console.WriteLine($"ERROR: Section with ID {section.Id} not found!");
TempData["ErrorMessage"] = "Section not found.";
return RedirectToAction("Index");
}
Console.WriteLine($"Found existing section: {existingSection.Title}");
// Update fields
existingSection.SectionType = section.SectionType;
existingSection.Title = section.Title ?? string.Empty;
existingSection.Subtitle = section.Subtitle ?? string.Empty;
existingSection.Content = section.Content ?? string.Empty;
existingSection.ButtonText = section.ButtonText ?? string.Empty;
existingSection.ButtonUrl = section.ButtonUrl ?? string.Empty;
existingSection.IsActive = section.IsActive;
existingSection.UpdatedAt = DateTime.UtcNow;
// Handle image upload
if (imageFile != null && imageFile.Length > 0)
{
var uploadsFolder = Path.Combine(_environment.WebRootPath, "uploads", "images");
Directory.CreateDirectory(uploadsFolder);
var uniqueFileName = $"{Guid.NewGuid()}_{imageFile.FileName}";
var filePath = Path.Combine(uploadsFolder, uniqueFileName);
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
await imageFile.CopyToAsync(fileStream);
}
existingSection.ImageUrl = $"/uploads/images/{uniqueFileName}";
Console.WriteLine($"New image uploaded: {existingSection.ImageUrl}");
}
await _mongoService.UpdateAsync(_sectionsCollection, existingSection.Id!, existingSection);
Console.WriteLine($"Section updated successfully in database");
TempData["SuccessMessage"] = "Section updated successfully!";
return RedirectToAction("Index");
}
[HttpGet("section/create")]
public IActionResult CreateSection()
{
return View();
}
[HttpPost("section/create")]
public async Task<IActionResult> CreateSection(HomepageSection section, IFormFile? imageFile)
{
// Remove Content validation error since it's optional for some section types
ModelState.Remove("Content");
ModelState.Remove("AdditionalData");
if (!ModelState.IsValid)
{
return View(section);
}
// Handle image upload
if (imageFile != null && imageFile.Length > 0)
{
var uploadsFolder = Path.Combine(_environment.WebRootPath, "uploads", "images");
Directory.CreateDirectory(uploadsFolder);
var uniqueFileName = $"{Guid.NewGuid()}_{imageFile.FileName}";
var filePath = Path.Combine(uploadsFolder, uniqueFileName);
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
await imageFile.CopyToAsync(fileStream);
}
section.ImageUrl = $"/uploads/images/{uniqueFileName}";
}
// Get the highest display order and add 1
var allSections = await _mongoService.GetAllAsync<HomepageSection>(_sectionsCollection);
section.DisplayOrder = allSections.Any() ? allSections.Max(s => s.DisplayOrder) + 1 : 0;
section.CreatedAt = DateTime.UtcNow;
section.UpdatedAt = DateTime.UtcNow;
await _mongoService.InsertAsync(_sectionsCollection, section);
TempData["SuccessMessage"] = "Section created successfully!";
return RedirectToAction("Index");
}
[HttpPost("section/delete/{id}")]
public async Task<IActionResult> DeleteSection(string id)
{
await _mongoService.DeleteAsync<HomepageSection>(_sectionsCollection, id);
TempData["SuccessMessage"] = "Section deleted successfully!";
return RedirectToAction("Index");
}
[HttpPost("section/reorder")]
public async Task<IActionResult> ReorderSections([FromBody] List<string> sectionIds)
{
for (int i = 0; i < sectionIds.Count; i++)
{
var section = await _mongoService.GetByIdAsync<HomepageSection>(_sectionsCollection, sectionIds[i]);
if (section != null)
{
section.DisplayOrder = i;
section.UpdatedAt = DateTime.UtcNow;
await _mongoService.UpdateAsync(_sectionsCollection, section.Id!, section);
}
}
return Json(new { success = true });
}
[HttpPost("section/toggle/{id}")]
public async Task<IActionResult> ToggleSection(string id)
{
var section = await _mongoService.GetByIdAsync<HomepageSection>(_sectionsCollection, id);
if (section != null)
{
section.IsActive = !section.IsActive;
section.UpdatedAt = DateTime.UtcNow;
await _mongoService.UpdateAsync(_sectionsCollection, section.Id!, section);
TempData["SuccessMessage"] = $"Section {(section.IsActive ? "activated" : "deactivated")} successfully!";
}
return RedirectToAction("Index");
}
[HttpPost("footer/update")]
public async Task<IActionResult> UpdateFooter(string footerText)
{
var settingsList = await _mongoService.GetAllAsync<SiteSettings>(_settingsCollection);
SiteSettings? settings = settingsList.FirstOrDefault();
if (settings == null)
{
settings = new SiteSettings { FooterText = footerText };
await _mongoService.InsertAsync(_settingsCollection, settings);
}
else
{
settings.FooterText = footerText;
settings.UpdatedAt = DateTime.UtcNow;
await _mongoService.UpdateAsync(_settingsCollection, settings.Id!, settings);
}
TempData["SuccessMessage"] = "Footer updated successfully!";
return RedirectToAction("Index");
}
}
}

View File

@@ -1,113 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
[Route("admin/menu")]
[Authorize(Roles = "Admin")]
public class AdminMenuController : Controller
{
private readonly MongoDBService _mongoService;
public AdminMenuController(MongoDBService mongoService)
{
_mongoService = mongoService;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
var menuItems = await _mongoService.GetAllAsync<MenuItem>("MenuItems");
return View(menuItems.OrderBy(m => m.DisplayOrder).ToList());
}
[HttpPost("reseed")]
public async Task<IActionResult> ReseedMenu()
{
// Delete all existing menu items
var existingItems = await _mongoService.GetAllAsync<MenuItem>("MenuItems");
foreach (var item in existingItems)
{
await _mongoService.DeleteAsync<MenuItem>("MenuItems", item.Id!);
}
// Add new menu items
var defaultMenuItems = new[]
{
new MenuItem { Label = "Home", Url = "/", DisplayOrder = 1, IsActive = true, ShowInNavbar = true, ShowInDropdown = true },
new MenuItem { Label = "Shop", Url = "/Shop", DisplayOrder = 2, IsActive = true, ShowInNavbar = true, ShowInDropdown = true },
new MenuItem { Label = "Top Sellers", Url = "/#top-sellers", DisplayOrder = 3, IsActive = true, ShowInNavbar = true, ShowInDropdown = true },
new MenuItem { Label = "Promotion", Url = "/#promotion", DisplayOrder = 4, IsActive = true, ShowInNavbar = true, ShowInDropdown = true },
new MenuItem { Label = "Portfolio", Url = "/Portfolio", DisplayOrder = 5, IsActive = true, ShowInNavbar = true, ShowInDropdown = true },
new MenuItem { Label = "Blog", Url = "/Blog", DisplayOrder = 6, IsActive = true, ShowInNavbar = true, ShowInDropdown = true },
new MenuItem { Label = "About", Url = "/About", DisplayOrder = 7, IsActive = true, ShowInNavbar = true, ShowInDropdown = true },
new MenuItem { Label = "Instagram", Url = "#instagram", DisplayOrder = 8, IsActive = true, ShowInNavbar = false, ShowInDropdown = true },
new MenuItem { Label = "Contact", Url = "/Contact", DisplayOrder = 9, IsActive = true, ShowInNavbar = true, ShowInDropdown = true },
new MenuItem { Label = "My Wishlist", Url = "#wishlist", DisplayOrder = 10, IsActive = true, ShowInNavbar = false, ShowInDropdown = true }
};
foreach (var item in defaultMenuItems)
{
await _mongoService.InsertAsync("MenuItems", item);
}
TempData["SuccessMessage"] = "Menu items reseeded successfully!";
return RedirectToAction("Index");
}
[HttpGet("create")]
public IActionResult Create()
{
return View(new MenuItem());
}
[HttpPost("create")]
public async Task<IActionResult> Create(MenuItem menuItem)
{
if (!ModelState.IsValid)
{
return View(menuItem);
}
menuItem.CreatedAt = DateTime.UtcNow;
await _mongoService.InsertAsync("MenuItems", menuItem);
TempData["SuccessMessage"] = "Menu item created successfully!";
return RedirectToAction("Index");
}
[HttpGet("edit/{id}")]
public async Task<IActionResult> Edit(string id)
{
var menuItem = await _mongoService.GetByIdAsync<MenuItem>("MenuItems", id);
if (menuItem == null)
{
return NotFound();
}
return View(menuItem);
}
[HttpPost("edit/{id}")]
public async Task<IActionResult> Edit(string id, MenuItem menuItem)
{
if (!ModelState.IsValid)
{
return View(menuItem);
}
menuItem.Id = id;
await _mongoService.UpdateAsync("MenuItems", id, menuItem);
TempData["SuccessMessage"] = "Menu item updated successfully!";
return RedirectToAction("Index");
}
[HttpPost("delete/{id}")]
public async Task<IActionResult> Delete(string id)
{
await _mongoService.DeleteAsync<MenuItem>("MenuItems", id);
TempData["SuccessMessage"] = "Menu item deleted successfully!";
return RedirectToAction("Index");
}
}
}

View File

@@ -1,167 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
[Route("admin/pages")]
[Authorize(Roles = "Admin")]
public class AdminPagesController : Controller
{
private readonly MongoDBService _mongoService;
private readonly SlugService _slugService;
private readonly string _pagesCollection = "Pages";
public AdminPagesController(MongoDBService mongoService, SlugService slugService)
{
_mongoService = mongoService;
_slugService = slugService;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
var pages = await _mongoService.GetAllAsync<Page>(_pagesCollection);
return View(pages.OrderBy(p => p.PageName).ToList());
}
[HttpGet("create")]
public IActionResult Create() => View(new Page());
[HttpPost("create")]
public async Task<IActionResult> Create(Page page)
{
if (!ModelState.IsValid)
{
return View(page);
}
page.CreatedAt = DateTime.UtcNow;
page.UpdatedAt = DateTime.UtcNow;
page.PageSlug = _slugService.GenerateSlug(page.PageName);
await _mongoService.InsertAsync(_pagesCollection, page);
TempData["SuccessMessage"] = "Page created successfully!";
return RedirectToAction("Index");
}
[HttpGet("edit/{id}")]
public async Task<IActionResult> Edit(string id)
{
var page = await _mongoService.GetByIdAsync<Page>(_pagesCollection, id);
if (page == null) return NotFound();
return View(page);
}
[HttpPost("edit/{id}")]
public async Task<IActionResult> Edit(string id, [FromForm] Page page, IFormCollection form)
{
Console.WriteLine("[ADMIN-PAGES] === FORM SUBMISSION DEBUG ===");
Console.WriteLine($"[ADMIN-PAGES] Form Keys: {string.Join(", ", form.Keys)}");
// Debug: Check what's in the form
foreach (var key in form.Keys)
{
if (key.StartsWith("ImageGallery") || key.StartsWith("TeamMembers"))
{
Console.WriteLine($"[ADMIN-PAGES] {key} = {form[key]}");
}
}
if (!ModelState.IsValid)
{
Console.WriteLine("[ADMIN-PAGES] ModelState is INVALID");
foreach (var error in ModelState.Values.SelectMany(v => v.Errors))
{
Console.WriteLine($"[ADMIN-PAGES] Error: {error.ErrorMessage}");
}
return View(page);
}
// Get existing page to preserve data
var existingPage = await _mongoService.GetByIdAsync<Page>(_pagesCollection, id);
if (existingPage == null)
{
Console.WriteLine($"[ADMIN-PAGES] Page not found: {id}");
return NotFound();
}
// Update basic fields
existingPage.PageName = page.PageName;
existingPage.Title = page.Title;
existingPage.Subtitle = page.Subtitle;
existingPage.Content = page.Content;
existingPage.IsActive = page.IsActive;
existingPage.UpdatedAt = DateTime.UtcNow;
existingPage.PageSlug = _slugService.GenerateSlug(page.PageName);
// Manually parse ImageGallery from form
existingPage.ImageGallery = new List<string>();
foreach (var key in form.Keys.Where(k => k.StartsWith("ImageGallery[")))
{
var value = form[key].ToString();
if (!string.IsNullOrEmpty(value))
{
existingPage.ImageGallery.Add(value);
}
}
// Manually parse TeamMembers from form
existingPage.TeamMembers = new List<TeamMember>();
var memberIndices = form.Keys
.Where(k => k.StartsWith("TeamMembers[") && k.Contains("].Name"))
.Select(k =>
{
var match = System.Text.RegularExpressions.Regex.Match(k, @"TeamMembers\[(\d+)\]");
return match.Success ? int.Parse(match.Groups[1].Value) : -1;
})
.Where(i => i >= 0)
.Distinct()
.OrderBy(i => i)
.ToList();
foreach (var index in memberIndices)
{
var member = new TeamMember
{
Name = form[$"TeamMembers[{index}].Name"].ToString(),
Role = form[$"TeamMembers[{index}].Role"].ToString(),
Bio = form[$"TeamMembers[{index}].Bio"].ToString(),
PhotoUrl = form[$"TeamMembers[{index}].PhotoUrl"].ToString()
};
existingPage.TeamMembers.Add(member);
}
Console.WriteLine($"[ADMIN-PAGES] Updating page: {existingPage.PageName} (Slug: {existingPage.PageSlug})");
Console.WriteLine($"[ADMIN-PAGES] Title: {existingPage.Title}");
Console.WriteLine($"[ADMIN-PAGES] Content length: {existingPage.Content?.Length ?? 0}");
Console.WriteLine($"[ADMIN-PAGES] Image Gallery Count: {existingPage.ImageGallery.Count}");
Console.WriteLine($"[ADMIN-PAGES] Team Members Count: {existingPage.TeamMembers.Count}");
if (existingPage.ImageGallery.Any())
{
Console.WriteLine($"[ADMIN-PAGES] Gallery Images: {string.Join(", ", existingPage.ImageGallery)}");
}
if (existingPage.TeamMembers.Any())
{
foreach (var member in existingPage.TeamMembers)
{
Console.WriteLine($"[ADMIN-PAGES] Team Member: {member.Name} - {member.Role} - Photo: {member.PhotoUrl}");
}
}
await _mongoService.UpdateAsync(_pagesCollection, id, existingPage);
TempData["SuccessMessage"] = "Page updated successfully!";
return RedirectToAction("Index");
}
[HttpPost("delete/{id}")]
public async Task<IActionResult> Delete(string id)
{
await _mongoService.DeleteAsync<Page>(_pagesCollection, id);
TempData["SuccessMessage"] = "Page deleted successfully!";
return RedirectToAction("Index");
}
}
}

View File

@@ -1,155 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
[Route("admin/portfolio")]
[Authorize(Roles="Admin")]
public class AdminPortfolioController : Controller
{
private readonly MongoDBService _mongoService;
private readonly SlugService _slugService;
private readonly string _categoriesCollection = "PortfolioCategories";
private readonly string _projectsCollection = "PortfolioProjects";
public AdminPortfolioController(MongoDBService mongoService, SlugService slugService)
{
_mongoService = mongoService;
_slugService = slugService;
}
[HttpGet("categories")]
public async Task<IActionResult> Categories()
{
var categories = await _mongoService.GetAllAsync<PortfolioCategory>(_categoriesCollection);
return View(categories.OrderBy(c => c.DisplayOrder).ToList());
}
[HttpGet("category/create")]
public IActionResult CreateCategory() => View(new PortfolioCategory());
[HttpPost("category/create")]
public async Task<IActionResult> CreateCategory(PortfolioCategory category)
{
if (!ModelState.IsValid)
{
return View(category);
}
category.CreatedAt = DateTime.UtcNow;
category.UpdatedAt = DateTime.UtcNow;
category.Slug = _slugService.GenerateSlug(category.Name);
await _mongoService.InsertAsync(_categoriesCollection, category);
TempData["SuccessMessage"] = "Category created successfully!";
return RedirectToAction("Categories");
}
[HttpGet("category/edit/{id}")]
public async Task<IActionResult> EditCategory(string id)
{
var category = await _mongoService.GetByIdAsync<PortfolioCategory>(_categoriesCollection, id);
if (category == null) return NotFound();
return View(category);
}
[HttpPost("category/edit/{id}")]
public async Task<IActionResult> EditCategory(string id, PortfolioCategory category)
{
if (!ModelState.IsValid)
{
return View(category);
}
category.Id = id;
category.UpdatedAt = DateTime.UtcNow;
category.Slug = _slugService.GenerateSlug(category.Name);
await _mongoService.UpdateAsync(_categoriesCollection, id, category);
TempData["SuccessMessage"] = "Category updated successfully!";
return RedirectToAction("Categories");
}
[HttpPost("category/delete/{id}")]
public async Task<IActionResult> DeleteCategory(string id)
{
await _mongoService.DeleteAsync<PortfolioCategory>(_categoriesCollection, id);
TempData["SuccessMessage"] = "Category deleted successfully!";
return RedirectToAction("Categories");
}
[HttpGet("projects")]
public async Task<IActionResult> Projects(string? categoryId)
{
var projects = await _mongoService.GetAllAsync<PortfolioProject>(_projectsCollection);
var categories = await _mongoService.GetAllAsync<PortfolioCategory>(_categoriesCollection);
if (!string.IsNullOrEmpty(categoryId))
{
projects = projects.Where(p => p.CategoryId == categoryId).ToList();
}
ViewBag.Categories = categories.Where(c => c.IsActive).ToList();
ViewBag.SelectedCategory = categoryId;
return View(projects.OrderBy(p => p.DisplayOrder).ToList());
}
[HttpGet("project/create")]
public async Task<IActionResult> CreateProject()
{
var categories = await _mongoService.GetAllAsync<PortfolioCategory>(_categoriesCollection);
ViewBag.Categories = categories.Where(c => c.IsActive).ToList();
return View(new PortfolioProject());
}
[HttpPost("project/create")]
public async Task<IActionResult> CreateProject(PortfolioProject project)
{
if (!ModelState.IsValid)
{
var categories = await _mongoService.GetAllAsync<PortfolioCategory>(_categoriesCollection);
ViewBag.Categories = categories.Where(c => c.IsActive).ToList();
return View(project);
}
project.CreatedAt = DateTime.UtcNow;
project.UpdatedAt = DateTime.UtcNow;
await _mongoService.InsertAsync(_projectsCollection, project);
TempData["SuccessMessage"] = "Project created successfully!";
return RedirectToAction("Projects");
}
[HttpGet("project/edit/{id}")]
public async Task<IActionResult> EditProject(string id)
{
var project = await _mongoService.GetByIdAsync<PortfolioProject>(_projectsCollection, id);
if (project == null) return NotFound();
var categories = await _mongoService.GetAllAsync<PortfolioCategory>(_categoriesCollection);
ViewBag.Categories = categories.Where(c => c.IsActive).ToList();
return View(project);
}
[HttpPost("project/edit/{id}")]
public async Task<IActionResult> EditProject(string id, PortfolioProject project)
{
if (!ModelState.IsValid)
{
var categories = await _mongoService.GetAllAsync<PortfolioCategory>(_categoriesCollection);
ViewBag.Categories = categories.Where(c => c.IsActive).ToList();
return View(project);
}
project.Id = id;
project.UpdatedAt = DateTime.UtcNow;
await _mongoService.UpdateAsync(_projectsCollection, id, project);
TempData["SuccessMessage"] = "Project updated successfully!";
return RedirectToAction("Projects");
}
[HttpPost("project/delete/{id}")]
public async Task<IActionResult> DeleteProject(string id)
{
await _mongoService.DeleteAsync<PortfolioProject>(_projectsCollection, id);
TempData["SuccessMessage"] = "Project deleted successfully!";
return RedirectToAction("Projects");
}
}
}

View File

@@ -1,172 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
[Route("admin/products")]
[Authorize(Roles = "Admin")]
public class AdminProductsController : Controller
{
private readonly MongoDBService _mongoService;
private readonly SlugService _slugService;
private readonly string _productsCollection = "Products";
public AdminProductsController(MongoDBService mongoService, SlugService slugService)
{
_mongoService = mongoService;
_slugService = slugService;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
var products = await _mongoService.GetAllAsync<Product>(_productsCollection);
return View(products.OrderByDescending(p => p.CreatedAt).ToList());
}
[HttpGet("create")]
public IActionResult Create() => View(new Product());
[HttpPost("create")]
public async Task<IActionResult> Create(Product product)
{
// Remove validation errors for optional fields
ModelState.Remove("ShortDescription");
ModelState.Remove("Description");
if (!ModelState.IsValid)
{
return View(product);
}
// Ensure checkbox defaults when unchecked
if (!Request.Form.ContainsKey("IsActive")) product.IsActive = false;
if (!Request.Form.ContainsKey("IsFeatured")) product.IsFeatured = false;
if (!Request.Form.ContainsKey("IsTopSeller")) product.IsTopSeller = false;
// Handle multiple colors from form
var colors = Request.Form["Colors"].Where(c => !string.IsNullOrEmpty(c)).Select(c => c!).ToList();
product.Colors = colors.Any() ? colors : new List<string>();
// Debug logging
Console.WriteLine($"[CREATE] Colors received: {colors.Count}");
if (colors.Any())
{
Console.WriteLine($"[CREATE] Colors: {string.Join(", ", colors)}");
}
product.CreatedAt = DateTime.UtcNow;
product.UpdatedAt = DateTime.UtcNow;
product.Slug = _slugService.GenerateSlug(product.Name);
await _mongoService.InsertAsync(_productsCollection, product);
TempData["SuccessMessage"] = "Product created successfully!";
return RedirectToAction("Index");
}
[HttpGet("edit/{id}")]
public async Task<IActionResult> Edit(string id)
{
var product = await _mongoService.GetByIdAsync<Product>(_productsCollection, id);
if (product == null) return NotFound();
return View("Create", product);
}
[HttpPost("edit/{id}")]
public async Task<IActionResult> Edit(string id, Product product)
{
// Remove validation errors for optional fields
ModelState.Remove("Images");
ModelState.Remove("Slug");
ModelState.Remove("ShortDescription");
ModelState.Remove("Description");
if (!ModelState.IsValid)
{
return View("Create", product);
}
// Ensure checkbox defaults when unchecked
if (!Request.Form.ContainsKey("IsActive")) product.IsActive = false;
if (!Request.Form.ContainsKey("IsFeatured")) product.IsFeatured = false;
if (!Request.Form.ContainsKey("IsTopSeller")) product.IsTopSeller = false;
// Get existing product to preserve data
var existingProduct = await _mongoService.GetByIdAsync<Product>(_productsCollection, id);
if (existingProduct == null)
{
TempData["ErrorMessage"] = "Product not found.";
return RedirectToAction("Index");
}
// Update editable fields
existingProduct.Name = product.Name;
existingProduct.ShortDescription = product.ShortDescription;
existingProduct.Description = product.Description;
existingProduct.Price = product.Price;
existingProduct.Category = product.Category;
existingProduct.Color = product.Color;
// Handle multiple colors from form
var colors = Request.Form["Colors"].Where(c => !string.IsNullOrEmpty(c)).Select(c => c!).ToList();
existingProduct.Colors = colors.Any() ? colors : new List<string>();
// Debug logging
Console.WriteLine($"[EDIT] Colors received: {colors.Count}");
if (colors.Any())
{
Console.WriteLine($"[EDIT] Colors: {string.Join(", ", colors)}");
}
Console.WriteLine($"[EDIT] Product Colors property: {existingProduct.Colors?.Count ?? 0}");
existingProduct.StockQuantity = product.StockQuantity;
existingProduct.IsFeatured = product.IsFeatured;
existingProduct.IsTopSeller = product.IsTopSeller;
existingProduct.IsActive = product.IsActive;
existingProduct.UpdatedAt = DateTime.UtcNow;
existingProduct.Slug = _slugService.GenerateSlug(product.Name);
// Update images
if (Request.Form.ContainsKey("Images"))
{
var images = Request.Form["Images"].Where(img => !string.IsNullOrEmpty(img)).Select(img => img!).ToList();
existingProduct.Images = images;
// Set first image as main ImageUrl if not explicitly set
if (images.Any() && string.IsNullOrEmpty(product.ImageUrl))
{
existingProduct.ImageUrl = images[0] ?? "";
}
}
// Preserve or update ImageUrl
if (!string.IsNullOrEmpty(product.ImageUrl))
{
existingProduct.ImageUrl = product.ImageUrl;
}
// Update SKU and CostPrice if provided
if (!string.IsNullOrEmpty(product.SKU))
{
existingProduct.SKU = product.SKU;
}
if (product.CostPrice > 0)
{
existingProduct.CostPrice = product.CostPrice;
}
await _mongoService.UpdateAsync(_productsCollection, id, existingProduct);
TempData["SuccessMessage"] = "Product updated successfully!";
return RedirectToAction("Index");
}
[HttpPost("delete/{id}")]
public async Task<IActionResult> Delete(string id)
{
await _mongoService.DeleteAsync<Product>(_productsCollection, id);
TempData["SuccessMessage"] = "Product deleted successfully!";
return RedirectToAction("Index");
}
}
}

View File

@@ -1,54 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
[Route("admin/settings")]
[Authorize(Roles="Admin")]
public class AdminSettingsController : Controller
{
private readonly MongoDBService _mongoService;
private readonly string _settingsCollection = "SiteSettings";
public AdminSettingsController(MongoDBService mongoService)
{
_mongoService = mongoService;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
var settingsList = await _mongoService.GetAllAsync<SiteSettings>(_settingsCollection);
var settings = settingsList.FirstOrDefault();
if (settings == null)
{
settings = new SiteSettings();
await _mongoService.InsertAsync(_settingsCollection, settings);
}
return View(settings);
}
[HttpPost("update")]
public async Task<IActionResult> Update(SiteSettings settings)
{
if (!ModelState.IsValid)
{
return View("Index", settings);
}
settings.UpdatedAt = DateTime.UtcNow;
if (!string.IsNullOrEmpty(settings.Id))
{
await _mongoService.UpdateAsync(_settingsCollection, settings.Id, settings);
}
else
{
await _mongoService.InsertAsync(_settingsCollection, settings);
}
TempData["SuccessMessage"] = "Site settings updated successfully!";
return RedirectToAction("Index");
}
}
}

View File

@@ -1,106 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
[Route("admin/upload")]
[Authorize(Roles="Admin")]
public class AdminUploadController : Controller
{
private readonly IWebHostEnvironment _environment;
public AdminUploadController(IWebHostEnvironment environment)
{
_environment = environment;
}
[HttpGet("")]
public IActionResult Index()
{
var uploadsPath = Path.Combine(_environment.WebRootPath, "uploads", "images");
var images = new List<string>();
if (Directory.Exists(uploadsPath))
{
var files = Directory.GetFiles(uploadsPath)
.Select(f => $"/uploads/images/{Path.GetFileName(f)}")
.OrderByDescending(f => f)
.ToList();
images = files;
}
return View(images);
}
[HttpPost("image")]
public async Task<IActionResult> UploadImage(IFormFile file)
{
if (file == null || file.Length == 0)
{
return Json(new { success = false, message = "No file uploaded" });
}
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" };
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!allowedExtensions.Contains(extension))
{
return Json(new { success = false, message = "Invalid file type" });
}
try
{
var uploadsPath = Path.Combine(_environment.WebRootPath, "uploads", "images");
if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath);
var fileName = $"{Guid.NewGuid()}{extension}";
var filePath = Path.Combine(uploadsPath, fileName);
using var stream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(stream);
return Json(new { success = true, url = $"/uploads/images/{fileName}" });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
[HttpPost("multiple")]
public async Task<IActionResult> UploadMultiple(List<IFormFile> files)
{
var uploadedUrls = new List<string>();
foreach (var file in files)
{
if (file == null || file.Length == 0) continue;
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" };
if (!allowedExtensions.Contains(extension)) continue;
var uploadsPath = Path.Combine(_environment.WebRootPath, "uploads", "images");
if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath);
var fileName = $"{Guid.NewGuid()}{extension}";
var filePath = Path.Combine(uploadsPath, fileName);
using var stream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(stream);
uploadedUrls.Add($"/uploads/images/{fileName}");
}
return Json(new { success = true, urls = uploadedUrls });
}
[HttpPost("delete")]
public IActionResult DeleteImage([FromBody] string imageUrl)
{
try
{
var fileName = Path.GetFileName(imageUrl);
var filePath = Path.Combine(_environment.WebRootPath, "uploads", "images", fileName);
if (System.IO.File.Exists(filePath))
{
System.IO.File.Delete(filePath);
return Json(new { success = true });
}
return Json(new { success = false, message = "File not found" });
}
catch (Exception ex)
{
return Json(new { success = false, message = ex.Message });
}
}
}
}

View File

@@ -1,62 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
namespace SkyArtShop.Controllers
{
[Route("api/upload")]
[Authorize(Roles = "Admin")]
public class ApiUploadController : Controller
{
private readonly IWebHostEnvironment _environment;
public ApiUploadController(IWebHostEnvironment environment)
{
_environment = environment;
}
[HttpPost("image")]
public async Task<IActionResult> UploadImage(IFormFile image)
{
if (image == null || image.Length == 0)
{
return Json(new { success = false, message = "No file uploaded" });
}
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" };
var extension = Path.GetExtension(image.FileName).ToLowerInvariant();
if (!allowedExtensions.Contains(extension))
{
return Json(new { success = false, message = "Invalid file type. Only images are allowed." });
}
try
{
var uploadsPath = Path.Combine(_environment.WebRootPath, "uploads", "images");
if (!Directory.Exists(uploadsPath))
{
Directory.CreateDirectory(uploadsPath);
}
var fileName = $"{Guid.NewGuid()}{extension}";
var filePath = Path.Combine(uploadsPath, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await image.CopyToAsync(stream);
}
var imageUrl = $"/uploads/images/{fileName}";
Console.WriteLine($"[API-UPLOAD] Image uploaded successfully: {imageUrl}");
return Json(new { success = true, imageUrl = imageUrl });
}
catch (Exception ex)
{
Console.WriteLine($"[API-UPLOAD] Upload failed: {ex.Message}");
return Json(new { success = false, message = $"Upload failed: {ex.Message}" });
}
}
}
}

View File

@@ -1,41 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
public class BlogController : Controller
{
private readonly MongoDBService _mongoService;
private readonly string _blogCollection = "BlogPosts";
public BlogController(MongoDBService mongoService)
{
_mongoService = mongoService;
}
public async Task<IActionResult> Index()
{
var posts = await _mongoService.GetAllAsync<BlogPost>(_blogCollection);
var publishedPosts = posts
.Where(p => p.IsPublished)
.OrderByDescending(p => p.PublishedDate)
.ToList();
return View(publishedPosts);
}
public async Task<IActionResult> Post(string slug)
{
var posts = await _mongoService.GetAllAsync<BlogPost>(_blogCollection);
var post = posts.FirstOrDefault(p => p.Slug == slug && p.IsPublished);
if (post == null)
{
return NotFound();
}
return View(post);
}
}
}

View File

@@ -1,34 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
public class ContactController : Controller
{
private readonly MongoDBService _mongoService;
private readonly string _settingsCollection = "SiteSettings";
public ContactController(MongoDBService mongoService)
{
_mongoService = mongoService;
}
public async Task<IActionResult> Index()
{
var settingsList = await _mongoService.GetAllAsync<SiteSettings>(_settingsCollection);
var settings = settingsList.FirstOrDefault() ?? new SiteSettings();
return View(settings);
}
[HttpPost]
public IActionResult Submit(string name, string email, string phone, string subject, string message)
{
// Here you would implement email sending logic
// For now, just return a success message
TempData["Success"] = "Thank you! Your message has been sent. We'll get back to you soon.";
return RedirectToAction("Index");
}
}
}

View File

@@ -1,36 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
[Route("diagnostics")]
public class DiagnosticsController : Controller
{
private readonly MongoDBService _mongoService;
public DiagnosticsController(MongoDBService mongoService)
{
_mongoService = mongoService;
}
[HttpGet("products")]
public async Task<IActionResult> Products()
{
var products = await _mongoService.GetAllAsync<Product>("Products");
var report = products.Select(p => new
{
p.Id,
p.Name,
p.ImageUrl,
ImagesCount = p.Images?.Count ?? 0,
FirstImage = p.Images?.FirstOrDefault(),
HasImageUrl = !string.IsNullOrEmpty(p.ImageUrl),
HasImages = p.Images != null && p.Images.Any()
}).ToList();
return Json(report);
}
}
}

View File

@@ -1,64 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
public class HomeController : Controller
{
private readonly MongoDBService _mongoService;
private readonly string _settingsCollection = "SiteSettings";
private readonly string _productsCollection = "Products";
private readonly string _sectionsCollection = "HomepageSections";
public HomeController(MongoDBService mongoService)
{
_mongoService = mongoService;
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task<IActionResult> Index()
{
var settings = await GetSiteSettings();
var topProducts = await GetTopSellerProducts();
var sections = await GetHomepageSections();
ViewBag.Settings = settings;
ViewBag.TopProducts = topProducts;
ViewBag.Sections = sections;
return View();
}
private async Task<SiteSettings> GetSiteSettings()
{
var settingsList = await _mongoService.GetAllAsync<SiteSettings>(_settingsCollection);
return settingsList.FirstOrDefault() ?? new SiteSettings();
}
private async Task<List<Product>> GetTopSellerProducts()
{
var products = await _mongoService.GetAllAsync<Product>(_productsCollection);
return products.Where(p => p.IsTopSeller && p.IsActive).Take(4).ToList();
}
private async Task<List<HomepageSection>> GetHomepageSections()
{
var sections = await _mongoService.GetAllAsync<HomepageSection>(_sectionsCollection);
Console.WriteLine($"Total sections from DB: {sections.Count}");
var activeSections = sections.Where(s => s.IsActive).OrderBy(s => s.DisplayOrder).ToList();
Console.WriteLine($"Active sections: {activeSections.Count}");
foreach (var section in activeSections)
{
Console.WriteLine($"Section: {section.Title} | Type: {section.SectionType} | Order: {section.DisplayOrder} | Active: {section.IsActive}");
}
return activeSections;
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View();
}
}
}

View File

@@ -1,31 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
[Route("page")]
public class PageController : Controller
{
private readonly MongoDBService _mongoService;
public PageController(MongoDBService mongoService)
{
_mongoService = mongoService;
}
[HttpGet("{slug}")]
public async Task<IActionResult> Index(string slug)
{
var pages = await _mongoService.GetAllAsync<Page>("Pages");
var page = pages.FirstOrDefault(p => p.PageSlug == slug && p.IsActive);
if (page == null)
{
return NotFound();
}
return View("View", page);
}
}
}

View File

@@ -1,58 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
public class PortfolioController : Controller
{
private readonly MongoDBService _mongoService;
private readonly string _categoriesCollection = "PortfolioCategories";
private readonly string _projectsCollection = "PortfolioProjects";
public PortfolioController(MongoDBService mongoService)
{
_mongoService = mongoService;
}
public async Task<IActionResult> Index()
{
var categories = await _mongoService.GetAllAsync<PortfolioCategory>(_categoriesCollection);
var activeCategories = categories.Where(c => c.IsActive).OrderBy(c => c.DisplayOrder).ToList();
return View(activeCategories);
}
public async Task<IActionResult> Category(string slug)
{
var categories = await _mongoService.GetAllAsync<PortfolioCategory>(_categoriesCollection);
var category = categories.FirstOrDefault(c => c.Slug == slug && c.IsActive);
if (category == null)
{
return NotFound();
}
var projects = await _mongoService.GetAllAsync<PortfolioProject>(_projectsCollection);
var categoryProjects = projects
.Where(p => p.CategoryId == category.Id && p.IsActive)
.OrderBy(p => p.DisplayOrder)
.ToList();
ViewBag.Category = category;
return View(categoryProjects);
}
public async Task<IActionResult> Project(string id)
{
var project = await _mongoService.GetByIdAsync<PortfolioProject>(_projectsCollection, id);
if (project == null)
{
return NotFound();
}
return View(project);
}
}
}

View File

@@ -1,102 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.Controllers
{
public class ShopController : Controller
{
private readonly MongoDBService _mongoService;
private readonly string _productsCollection = "Products";
public ShopController(MongoDBService mongoService)
{
_mongoService = mongoService;
}
public async Task<IActionResult> Index(string? category, string? sort)
{
var products = await _mongoService.GetAllAsync<Product>(_productsCollection);
var activeProducts = products.Where(p => p.IsActive).ToList();
// Filter by category if specified
if (!string.IsNullOrEmpty(category) && category != "all")
{
activeProducts = activeProducts.Where(p => p.Category == category).ToList();
}
// Sort products
activeProducts = sort switch
{
"price-low" => activeProducts.OrderBy(p => p.Price).ToList(),
"price-high" => activeProducts.OrderByDescending(p => p.Price).ToList(),
"newest" => activeProducts.OrderByDescending(p => p.CreatedAt).ToList(),
_ => activeProducts.OrderByDescending(p => p.IsFeatured).ToList()
};
ViewBag.SelectedCategory = category ?? "all";
ViewBag.SelectedSort = sort ?? "featured";
ViewBag.Categories = activeProducts.Select(p => p.Category).Distinct().ToList();
return View(activeProducts);
}
public async Task<IActionResult> Product(string id)
{
var product = await _mongoService.GetByIdAsync<Product>(_productsCollection, id);
if (product == null)
{
return NotFound();
}
// Debug logging
Console.WriteLine($"[SHOP] Product ID: {id}");
Console.WriteLine($"[SHOP] Product Name: {product.Name}");
Console.WriteLine($"[SHOP] Colors Count: {product.Colors?.Count ?? 0}");
if (product.Colors != null && product.Colors.Any())
{
Console.WriteLine($"[SHOP] Colors: {string.Join(", ", product.Colors)}");
}
Console.WriteLine($"[SHOP] Legacy Color: {product.Color ?? "null"}");
// Track product view
var sessionId = HttpContext.Session.Id;
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
var productView = new ProductView
{
ProductId = id,
SessionId = sessionId,
IpAddress = ipAddress,
ViewedAt = DateTime.UtcNow
};
await _mongoService.InsertAsync("ProductViews", productView);
// Get related products based on:
// 1. Same category
// 2. Most viewed products
// 3. Exclude current product
var allProducts = await _mongoService.GetAllAsync<Product>(_productsCollection);
var allViews = await _mongoService.GetAllAsync<ProductView>("ProductViews");
// Count views for each product
var productViewCounts = allViews
.GroupBy(v => v.ProductId)
.ToDictionary(g => g.Key, g => g.Count());
var relatedProducts = allProducts
.Where(p => p.IsActive && p.Id != id)
.OrderByDescending(p =>
(p.Category == product.Category ? 100 : 0) + // Same category bonus
(productViewCounts.ContainsKey(p.Id ?? "") ? productViewCounts[p.Id ?? ""] : 0) + // View count
(p.IsFeatured ? 50 : 0) + // Featured bonus
(p.UnitsSold * 2) // Sales popularity
)
.Take(4)
.ToList();
ViewBag.RelatedProducts = relatedProducts;
return View(product);
}
}
}

View File

@@ -1,200 +0,0 @@
# Sky Art Shop - Quick Deployment Checklist
## 🎯 Pre-Deployment Setup (One-Time)
### 1. Install Prerequisites
- [ ] **Download .NET 8.0 Hosting Bundle**
- Visit: <https://dotnet.microsoft.com/download/dotnet/8.0>
- Look for "Hosting Bundle" under Windows
- Install and restart computer
- [ ] **Enable IIS** (Run PowerShell as Administrator)
```powershell
.\deploy.ps1 -InstallIIS
```
- Restart computer after installation
- [ ] **Install MongoDB** (if not already)
- Download from: <https://www.mongodb.com/try/download/community>
- Install as Windows Service
- Verify it's running: `net start MongoDB`
### 2. Network Configuration
- [ ] **Set Static Local IP**
1. Control Panel → Network → Change adapter settings
2. Right-click network → Properties → IPv4 → Properties
3. Use static IP: e.g., `192.168.1.100`
- [ ] **Configure Router Port Forwarding**
1. Access router (usually <http://192.168.1.1>)
2. Find Port Forwarding section
3. Forward: External Port 80 → Internal IP 192.168.1.100:80
- [ ] **Install No-IP DUC Client**
1. Download: <https://www.noip.com/download>
2. Install and login with No-IP credentials
3. Verify it's updating your hostname
### 3. Security Setup
- [ ] **Change Admin Password**
- Edit: `appsettings.Production.json`
- Update the password field
- Use a strong password!
## 🚀 Deployment Steps (Every Time You Update)
### Option A: Automated Deployment (Recommended)
Run PowerShell as **Administrator**:
```powershell
cd "E:\Documents\Website Projects\Sky_Art_Shop"
# First time deployment (creates IIS site)
.\deploy.ps1 -CreateSite
# Future updates (faster, just updates files)
.\deploy.ps1 -UpdateOnly
```
### Option B: Manual Deployment
Run PowerShell as **Administrator**:
```powershell
# 1. Stop IIS site (if updating)
Stop-WebSite -Name "SkyArtShop"
# 2. Publish application
cd "E:\Documents\Website Projects\Sky_Art_Shop"
dotnet publish -c Release -o "C:\inetpub\wwwroot\skyartshop"
# 3. Set permissions
icacls "C:\inetpub\wwwroot\skyartshop" /grant "IIS_IUSRS:(OI)(CI)F" /T
icacls "C:\inetpub\wwwroot\skyartshop\wwwroot\uploads" /grant "IIS_IUSRS:(OI)(CI)F" /T
# 4. Start IIS site
Start-WebSite -Name "SkyArtShop"
```
## ✅ Testing
### Test 1: Local (on server)
- [ ] Open browser on server machine
- [ ] Visit: <http://localhost>
- [ ] ✅ Site loads correctly
### Test 2: Local Network
- [ ] On another device (phone/laptop on same WiFi)
- [ ] Visit: <http://192.168.1.100> (your server's local IP)
- [ ] ✅ Site loads correctly
### Test 3: Internet
- [ ] On mobile data or different network
- [ ] Visit: <http://your-hostname.ddns.net> (your No-IP hostname)
- [ ] ✅ Site loads from internet
## 🔍 Troubleshooting
### Site Not Loading Locally
```powershell
# Check IIS site status
Get-WebSite -Name "SkyArtShop"
# Check if port 80 is listening
netstat -ano | findstr :80
# Restart IIS
iisreset /restart
```
### Site Not Loading from Internet
- [ ] Verify No-IP DUC is running (check system tray)
- [ ] Check router port forwarding is configured
- [ ] Test your public IP: <https://www.whatismyip.com>
- [ ] Visit: http://YOUR_PUBLIC_IP (should show your site)
### MongoDB Connection Error
```powershell
# Check MongoDB status
net start MongoDB
# If not running, start it
net start MongoDB
```
### Permission Errors (403/500)
```powershell
# Re-apply permissions
icacls "C:\inetpub\wwwroot\skyartshop" /grant "IIS_IUSRS:(OI)(CI)F" /T
icacls "C:\inetpub\wwwroot\skyartshop" /grant "IUSR:(OI)(CI)F" /T
# Restart IIS
iisreset /restart
```
## 📊 Quick Commands
```powershell
# Check site status
Get-WebSite -Name "SkyArtShop" | Select Name, State
# Start site
Start-WebSite -Name "SkyArtShop"
# Stop site
Stop-WebSite -Name "SkyArtShop"
# Restart IIS
iisreset /restart
# Check MongoDB
net start MongoDB
# View firewall rules
Get-NetFirewallRule -DisplayName "*SkyArtShop*"
# Check what's using port 80
netstat -ano | findstr :80
```
## 🎯 Your Site URLs
After deployment, your site will be accessible at:
- **Local Machine**: <http://localhost>
- **Local Network**: <http://192.168.1.100> (or your static IP)
- **Internet**: <http://your-hostname.ddns.net> (your No-IP hostname)
## 📝 Notes
- **Development**: Continue using `dotnet run` on port 5001 for local development
- **Production**: Clients access via your No-IP hostname
- **Updates**: Run `.\deploy.ps1 -UpdateOnly` to push changes
- **Backups**: Consider backing up `identity.db` and MongoDB before major updates
## 🎉 Success Criteria
✅ Site loads on localhost
✅ Site loads on local network
✅ Site loads from internet via No-IP hostname
✅ Admin login works
✅ Images upload correctly
✅ MongoDB data persists
✅ No error messages in browser console
---
**Ready to deploy? Start with the Pre-Deployment Setup, then run the automated deployment script!**

View File

@@ -1,493 +0,0 @@
# Sky Art Shop - Web Deployment Guide
**Target:** XAMPP v3.3.0 + No-IP Dynamic DNS
**Date:** December 3, 2025
---
## 🎯 Deployment Overview
This guide will help you deploy your ASP.NET Core application to the web so clients can view it while you continue development locally.
### Architecture
```
Internet → No-IP DNS → Your Public IP → Router Port Forward → XAMPP/IIS → ASP.NET Core App
```
---
## ⚠️ Important Notes
1. **XAMPP Limitation**: XAMPP is primarily for PHP applications. For ASP.NET Core, we'll use **IIS (Internet Information Services)** or **Kestrel** as a Windows Service.
2. **No-IP**: Your dynamic DNS will point to your public IP address
3. **Port Forwarding**: You'll need to forward ports 80 (HTTP) and 443 (HTTPS) on your router
4. **Security**: This setup is for preview purposes. For production, use a proper hosting service.
---
## 📋 Prerequisites
- ✅ Windows 10/11 with IIS or Windows Server
- ✅ .NET 8.0 Runtime installed
- ✅ MongoDB running on localhost:27017
- ✅ No-IP account configured with your hostname
- ✅ Router access for port forwarding
- ✅ Static local IP for your server machine
---
## 🚀 Option 1: Deploy with IIS (Recommended)
### Step 1: Enable IIS on Windows
1. Open **Control Panel****Programs****Turn Windows features on or off**
2. Check these features:
- ✅ Internet Information Services
- ✅ Web Management Tools
- ✅ World Wide Web Services
- ✅ Application Development Features
- ✅ .NET Extensibility 4.8
- ✅ ASP.NET 4.8
- ✅ IIS Management Console
3. Click **OK** and wait for installation
### Step 2: Install ASP.NET Core Hosting Bundle
1. Download from: <https://dotnet.microsoft.com/download/dotnet/8.0>
2. Look for "Hosting Bundle" (includes .NET Runtime and IIS support)
3. Install and restart your computer
### Step 3: Publish Your Application
Run these commands in PowerShell:
```powershell
cd "E:\Documents\Website Projects\Sky_Art_Shop"
dotnet publish SkyArtShop.csproj -c Release -o "C:\inetpub\wwwroot\skyartshop"
```
**Note:** Make sure to run each command separately, pressing Enter after each one.
### Step 4: Configure Application Settings for Production
Create production settings:
```powershell
# Copy appsettings.json to production version
Copy-Item "appsettings.json" "C:\inetpub\wwwroot\skyartshop\appsettings.Production.json"
```
### Step 5: Create IIS Site
1. Open **IIS Manager** (search in Windows Start)
2. Expand your server name → Right-click **Sites****Add Website**
3. Configure:
- **Site name**: SkyArtShop
- **Physical path**: `C:\inetpub\wwwroot\skyartshop`
- **Binding**:
- Type: http
- IP: All Unassigned
- Port: 80
- Host name: (leave empty or add your No-IP hostname)
4. Click **OK**
### Step 6: Configure Application Pool
1. In IIS Manager, click **Application Pools**
2. Find **SkyArtShop** pool → Right-click → **Basic Settings**
3. Set **.NET CLR version**: **No Managed Code**
4. Click **OK**
5. Right-click pool → **Advanced Settings**
6. Set **Start Mode**: **AlwaysRunning**
7. Set **Identity**: **ApplicationPoolIdentity** or your user account
8. Click **OK**
### Step 7: Set Permissions
```powershell
# Grant IIS permissions to your application folder
icacls "C:\inetpub\wwwroot\skyartshop" /grant "IIS_IUSRS:(OI)(CI)F" /T
icacls "C:\inetpub\wwwroot\skyartshop" /grant "IUSR:(OI)(CI)F" /T
# Grant permissions to uploads folder
icacls "C:\inetpub\wwwroot\skyartshop\wwwroot\uploads" /grant "IIS_IUSRS:(OI)(CI)F" /T
```
---
## 🚀 Option 2: Deploy as Windows Service with Kestrel
### Step 1: Publish Application
```powershell
cd "E:\Documents\Website Projects\Sky_Art_Shop"
dotnet publish SkyArtShop.csproj -c Release -o "C:\Services\SkyArtShop"
```
### Step 2: Install as Windows Service
```powershell
# Install the service using sc.exe command
sc.exe create SkyArtShopService binPath="C:\Services\SkyArtShop\SkyArtShop.exe" start=auto
# Or use NSSM (Non-Sucking Service Manager) - Download from nssm.cc
# Download from: https://nssm.cc/download
nssm install SkyArtShopService "C:\Services\SkyArtShop\SkyArtShop.exe"
```
### Step 3: Configure Service
```powershell
# Set service to start automatically
sc.exe config SkyArtShopService start=auto
# Start the service
sc.exe start SkyArtShopService
# Check service status
sc.exe query SkyArtShopService
```
---
## 🌐 Network Configuration
### Step 1: Set Static Local IP
1. Open **Control Panel****Network and Sharing Center**
2. Click your network connection → **Properties**
3. Select **Internet Protocol Version 4 (TCP/IPv4)****Properties**
4. Choose **Use the following IP address**:
- IP: `192.168.1.100` (or similar, check your router's range)
- Subnet: `255.255.255.0`
- Gateway: Your router IP (usually `192.168.1.1`)
- DNS: `8.8.8.8` (Google DNS)
### Step 2: Configure Router Port Forwarding
1. Access your router admin panel (usually <http://192.168.1.1>)
2. Find **Port Forwarding** or **Virtual Server** settings
3. Add new rule:
- **Service Name**: SkyArtShop-HTTP
- **External Port**: 80
- **Internal IP**: 192.168.1.100 (your server's static IP)
- **Internal Port**: 80
- **Protocol**: TCP
4. Add HTTPS rule (optional):
- **Service Name**: SkyArtShop-HTTPS
- **External Port**: 443
- **Internal IP**: 192.168.1.100
- **Internal Port**: 443
- **Protocol**: TCP
5. Save settings
### Step 3: Configure Windows Firewall
```powershell
# Allow HTTP traffic
New-NetFirewallRule -DisplayName "SkyArtShop-HTTP" -Direction Inbound -Protocol TCP -LocalPort 80 -Action Allow
# Allow HTTPS traffic (optional)
New-NetFirewallRule -DisplayName "SkyArtShop-HTTPS" -Direction Inbound -Protocol TCP -LocalPort 443 -Action Allow
# Or allow the application itself
New-NetFirewallRule -DisplayName "SkyArtShop-App" -Direction Inbound -Program "C:\inetpub\wwwroot\skyartshop\SkyArtShop.exe" -Action Allow
```
### Step 4: Update No-IP Configuration
1. Log in to your No-IP account (<https://www.noip.com>)
2. Go to **Dynamic DNS****Hostnames**
3. Your hostname (e.g., `yoursite.ddns.net`) should already be configured
4. Install **No-IP DUC (Dynamic Update Client)** to keep your IP updated:
- Download from: <https://www.noip.com/download>
- Install and configure with your No-IP credentials
- It will automatically update your DNS when your public IP changes
---
## ⚙️ Update Application Configuration
### 1. Update appsettings.Production.json
```json
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"MongoDB": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "SkyArtShopDB"
},
"AdminUser": {
"Email": "admin@skyartshop.com",
"Password": "Admin123!",
"Name": "Sky Art Shop Admin"
}
}
```
### 2. Update Program.cs for Production URLs
The app should listen on all interfaces in production. This is already configured in your Program.cs:
```csharp
builder.WebHost.UseUrls("http://localhost:5001");
```
Change to:
```csharp
// Allow configuration from environment or use default
if (builder.Environment.IsProduction())
{
builder.WebHost.UseUrls("http://*:80", "https://*:443");
}
else
{
builder.WebHost.UseUrls("http://localhost:5001");
}
```
---
## 🔒 Security Considerations
### 1. Change Admin Password
Update `appsettings.Production.json` with a strong password:
```json
"AdminUser": {
"Email": "admin@skyartshop.com",
"Password": "YourStrongPassword123!@#",
"Name": "Sky Art Shop Admin"
}
```
### 2. Configure HTTPS (Recommended)
For HTTPS, you'll need an SSL certificate:
**Option A: Free SSL with Let's Encrypt**
1. Use **Certify The Web** (free for IIS): <https://certifytheweb.com>
2. Install and configure with your No-IP domain
3. It will automatically obtain and renew certificates
**Option B: Self-Signed Certificate (for testing)**
```powershell
# Create self-signed certificate
New-SelfSignedCertificate -DnsName "yoursite.ddns.net" -CertStoreLocation "cert:\LocalMachine\My"
```
### 3. Disable Development Features
Ensure `ASPNETCORE_ENVIRONMENT` is set to `Production`:
```powershell
# For IIS (web.config)
# This is automatically set when you publish with -c Release
# For Windows Service
[Environment]::SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Production", "Machine")
```
---
## 📁 File Structure After Deployment
```
C:\inetpub\wwwroot\skyartshop\ (or C:\Services\SkyArtShop\)
├── SkyArtShop.exe
├── SkyArtShop.dll
├── appsettings.json
├── appsettings.Production.json
├── web.config (auto-generated for IIS)
├── wwwroot\
│ ├── assets\
│ ├── uploads\
│ └── ...
└── ... (other dependencies)
```
---
## 🧪 Testing Your Deployment
### Local Testing (on server machine)
1. Open browser on server
2. Navigate to: `http://localhost` or `http://127.0.0.1`
3. You should see your site
### Network Testing (from another device on same network)
1. Find your server's local IP: `192.168.1.100`
2. On another device (phone/laptop on same WiFi)
3. Navigate to: `http://192.168.1.100`
### Internet Testing (from outside your network)
1. Use your No-IP hostname: `http://yoursite.ddns.net`
2. Test from mobile data or ask a friend to visit
3. Should load your site from the internet
### Troubleshooting Commands
```powershell
# Check if port 80 is listening
netstat -ano | findstr :80
# Check IIS site status
Get-WebSite -Name "SkyArtShop"
# Check Windows Service status
Get-Service -Name "SkyArtShopService"
# View application logs
Get-EventLog -LogName Application -Source "IIS*" -Newest 20
# Test if MongoDB is accessible
Test-NetConnection -ComputerName localhost -Port 27017
```
---
## 🔄 Updating Your Site (While Keeping it Live)
When you want to update the site:
### Method 1: Quick Update (IIS)
```powershell
# 1. Stop IIS site
Stop-WebSite -Name "SkyArtShop"
# 2. Publish new version
cd "E:\Documents\Website Projects\Sky_Art_Shop"
dotnet publish SkyArtShop.csproj -c Release -o "C:\inetpub\wwwroot\skyartshop"
# 3. Start IIS site
Start-WebSite -Name "SkyArtShop"
```
### Method 2: Zero-Downtime Update
```powershell
# 1. Publish to new folder
dotnet publish SkyArtShop.csproj -c Release -o "C:\inetpub\wwwroot\skyartshop_new"
# 2. Stop site
Stop-WebSite -Name "SkyArtShop"
# 3. Backup old version
Rename-Item "C:\inetpub\wwwroot\skyartshop" "skyartshop_backup"
# 4. Switch to new version
Rename-Item "C:\inetpub\wwwroot\skyartshop_new" "skyartshop"
# 5. Start site
Start-WebSite -Name "SkyArtShop"
# 6. Test and remove backup if successful
# Remove-Item "C:\inetpub\wwwroot\skyartshop_backup" -Recurse
```
---
## 📊 Monitoring
### Check Application Status
```powershell
# IIS
Get-WebSite -Name "SkyArtShop" | Select-Object Name, State, PhysicalPath
# Windows Service
Get-Service -Name "SkyArtShopService" | Select-Object Name, Status, StartType
```
### View Logs
Logs location:
- **IIS**: `C:\inetpub\wwwroot\skyartshop\logs\` (if configured)
- **Windows Event Viewer**: Application logs
- **MongoDB**: Check MongoDB logs for database issues
---
## 🎯 Quick Deployment Checklist
- [ ] .NET 8.0 Hosting Bundle installed
- [ ] IIS enabled and configured
- [ ] Application published to IIS folder
- [ ] Application pool configured (No Managed Code)
- [ ] Folder permissions set (IIS_IUSRS)
- [ ] Static local IP configured
- [ ] Router port forwarding setup (port 80)
- [ ] Windows Firewall rules added
- [ ] No-IP DUC client installed and running
- [ ] MongoDB running on localhost
- [ ] Production settings configured
- [ ] Admin password changed
- [ ] Site tested locally
- [ ] Site tested from local network
- [ ] Site tested from internet (No-IP hostname)
---
## 🆘 Common Issues & Solutions
### Issue 1: HTTP Error 502.5
**Cause**: ASP.NET Core Runtime not installed
**Solution**: Install .NET 8.0 Hosting Bundle
### Issue 2: Site Not Accessible from Internet
**Cause**: Port forwarding not configured
**Solution**: Check router settings, ensure port 80 is forwarded to correct local IP
### Issue 3: 403 Forbidden Error
**Cause**: Permissions issue
**Solution**: Run the icacls commands to grant IIS permissions
### Issue 4: MongoDB Connection Failed
**Cause**: MongoDB not running
**Solution**: Start MongoDB service: `net start MongoDB`
### Issue 5: No-IP Hostname Not Resolving
**Cause**: No-IP DUC not running or IP not updated
**Solution**: Install/restart No-IP DUC client
---
## 📞 Support Resources
- **IIS Documentation**: <https://docs.microsoft.com/en-us/iis>
- **ASP.NET Core Hosting**: <https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/>
- **No-IP Support**: <https://www.noip.com/support>
- **Let's Encrypt**: <https://letsencrypt.org>
---
## 🎉 Success
Once deployed, your site will be accessible at:
- **Local**: <http://localhost>
- **Network**: <http://192.168.1.100> (your local IP)
- **Internet**: <http://yoursite.ddns.net> (your No-IP hostname)
Clients can view the site while you continue development locally on port 5001! 🚀

View File

@@ -1,18 +0,0 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace SkyArtShop.Data
{
public class ApplicationUser : IdentityUser
{
public string DisplayName { get; set; } = string.Empty;
}
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
}
}

View File

@@ -1,234 +0,0 @@
# Sky Art Shop - Image Requirements Guide
This document outlines all the images needed for the Sky Art Shop website. Use this as a checklist when gathering or creating images.
## 📸 Image Specifications
### General Guidelines
- **Format**: JPG for photos, PNG for logos/graphics
- **Quality**: High resolution (minimum 1200px wide for hero images)
- **Optimization**: Compress images to reduce file size without losing quality
- **Naming**: Use descriptive, lowercase names with hyphens (e.g., `washi-tape-collection.jpg`)
## 🏠 Home Page Images
### Hero Section
- **hero-craft.jpg** (1920x1080px recommended)
- Main hero image showing scrapbooking/crafting
- Should be bright, inviting, and showcase products
### Inspiration Section
- **craft-supplies.jpg** (800x600px)
- Display of craft supplies, journals, stickers, etc.
### Collection Grid (4 images)
- **washi-tape.jpg** (400x400px)
- **stickers.jpg** (400x400px)
- **journals.jpg** (400x400px)
- **cardmaking.jpg** (400x400px)
### Top Sellers (4 product images)
- **products/product-1.jpg** (500x500px)
- **products/product-2.jpg** (500x500px)
- **products/product-3.jpg** (500x500px)
- **products/product-4.jpg** (500x500px)
## 🎨 Portfolio Page Images
### Category Thumbnails (4 images)
- **portfolio/displays.jpg** (600x800px)
- Show display boards or craft fair setups
- **portfolio/personal-crafts.jpg** (600x800px)
- Personal craft projects, decorated journals
- **portfolio/card-making.jpg** (600x800px)
- Handmade cards collection
- **portfolio/scrapbook-albums.jpg** (600x800px)
- Scrapbook album pages
### Category Projects
#### Displays (4 images)
- **portfolio/displays/project-1.jpg** (800x600px)
- **portfolio/displays/project-2.jpg** (800x600px)
- **portfolio/displays/project-3.jpg** (800x600px)
- **portfolio/displays/project-4.jpg** (800x600px)
#### Personal Craft Projects (6 images)
- **portfolio/personal-crafts/project-1.jpg** (800x600px)
- **portfolio/personal-crafts/project-2.jpg** (800x600px)
- **portfolio/personal-crafts/project-3.jpg** (800x600px)
- **portfolio/personal-crafts/project-4.jpg** (800x600px)
- **portfolio/personal-crafts/project-5.jpg** (800x600px)
- **portfolio/personal-crafts/project-6.jpg** (800x600px)
#### Card Making (6 images)
- **portfolio/card-making/project-1.jpg** (800x600px)
- **portfolio/card-making/project-2.jpg** (800x600px)
- **portfolio/card-making/project-3.jpg** (800x600px)
- **portfolio/card-making/project-4.jpg** (800x600px)
- **portfolio/card-making/project-5.jpg** (800x600px)
- **portfolio/card-making/project-6.jpg** (800x600px)
#### Scrapbook Albums (6 images)
- **portfolio/scrapbook-albums/project-1.jpg** (800x600px)
- **portfolio/scrapbook-albums/project-2.jpg** (800x600px)
- **portfolio/scrapbook-albums/project-3.jpg** (800x600px)
- **portfolio/scrapbook-albums/project-4.jpg** (800x600px)
- **portfolio/scrapbook-albums/project-5.jpg** (800x600px)
- **portfolio/scrapbook-albums/project-6.jpg** (800x600px)
## 🛍️ Shop Page Images (12 product images)
### Washi Tape
- **products/washi-tape-1.jpg** (500x500px) - Floral set
- **products/washi-tape-2.jpg** (500x500px) - Geometric patterns
- **products/washi-tape-3.jpg** (500x500px) - Pastel collection
### Stickers
- **products/sticker-pack-1.jpg** (500x500px) - Vintage stickers
- **products/sticker-pack-2.jpg** (500x500px) - Nature themed
- **products/sticker-pack-3.jpg** (500x500px) - Kawaii bundle
### Journals
- **products/journal-1.jpg** (500x500px) - Dotted journal
- **products/journal-2.jpg** (500x500px) - Travel journal
- **products/journal-3.jpg** (500x500px) - Gratitude journal
### Other Products
- **products/card-kit-1.jpg** (500x500px) - Card making kit
- **products/card-paper-1.jpg** (500x500px) - Card stock
- **products/scrapbook-kit-1.jpg** (500x500px) - Scrapbook album kit
## About Page Images (2 images)
- **about-1.jpg** (600x400px) - Crafting supplies or workspace
- **about-2.jpg** (600x400px) - Creative workspace or products
## 📝 Blog Page Images (6 images)
- **blog/blog-1.jpg** (800x600px) - Washi tape ideas
- **blog/blog-2.jpg** (800x600px) - Journal setup
- **blog/blog-3.jpg** (800x600px) - Card making
- **blog/blog-4.jpg** (800x600px) - Mental health/crafting
- **blog/blog-5.jpg** (800x600px) - Scrapbooking techniques
- **blog/blog-6.jpg** (800x600px) - Collage art
## 🎯 Tips for Creating/Selecting Images
### Photography Tips
1. **Lighting**: Use natural light when possible, avoid harsh shadows
2. **Composition**: Keep main subject centered or follow rule of thirds
3. **Background**: Use clean, uncluttered backgrounds
4. **Colors**: Vibrant but not oversaturated colors work best
5. **Focus**: Ensure images are sharp and in focus
### Stock Photo Resources (Free)
- Unsplash.com
- Pexels.com
- Pixabay.com
- Freepik.com (some free options)
### Search Terms for Stock Photos
- "scrapbooking supplies"
- "washi tape collection"
- "bullet journal"
- "craft supplies flat lay"
- "handmade cards"
- "stationery collection"
- "journaling desk"
- "creative workspace"
### Image Optimization Tools
- **TinyPNG.com** - Compress JPG/PNG files
- **Squoosh.app** - Google's image compression tool
- **ImageOptim** - Mac app for optimization
- **GIMP** - Free image editing software
## 📦 Placeholder Images
Until you have your own images, you can use placeholder services:
- **Lorem Picsum**: <https://picsum.photos/800/600>
- **Unsplash Source**: <https://source.unsplash.com/800x600/?crafts>
- **Placeholder.com**: <https://via.placeholder.com/800x600>
Example usage in HTML:
```html
<img src="https://picsum.photos/800/600" alt="Placeholder">
```
## ✅ Image Checklist
Use this checklist to track your progress:
### Home Page
- [ ] Hero image
- [ ] Craft supplies image
- [ ] 4 Collection grid images
- [ ] 4 Product images
### Portfolio
- [ ] 4 Category thumbnails
- [ ] 4 Displays projects
- [ ] 6 Personal craft projects
- [ ] 6 Card making projects
- [ ] 6 Scrapbook album projects
### Shop
- [ ] 12 Product images
### About
- [ ] 2 About page images
### Blog
- [ ] 6 Blog post images
**Total: 59 images needed**
## 🔄 Updating Images
To update images in the website:
1. Place your images in the appropriate folder under `assets/images/`
2. Ensure the filename matches what's referenced in the HTML
3. Optimize the image for web (compress, resize if needed)
4. Clear browser cache to see changes
## 📝 Adding Alt Text
For accessibility, add descriptive alt text to all images:
```html
<img src="path/to/image.jpg" alt="Colorful washi tape collection on wooden desk">
```
Good alt text:
- Describes the image content
- Is concise (under 125 characters)
- Includes relevant keywords naturally
- Helps visually impaired users understand the content

View File

@@ -1,128 +0,0 @@
# 🔧 Quick Fix: Images Not Showing
## Issue
Products exist in database but `mainImageUrl` is empty, so images don't appear on the website.
## Root Cause
When you created products, either:
1. Images weren't selected during creation
2. Form wasn't set to `multipart/form-data`
3. Upload failed silently
## ✅ Solution: Re-upload Images via Admin
### Step 1: Start Backend (if not running)
```powershell
cd "e:\Documents\Website Projects\Sky_Art_Shop\Admin"
dotnet run --launch-profile https
```
### Step 2: Access Admin & Edit Products
1. Open: <https://localhost:5001/admin/products>
2. Click **Edit** on each product
3. Upload images:
- **Main Image**: The primary product image
- **Gallery Images** (optional): Additional photos
4. Click **Save**
The form will:
- Upload images to `wwwroot/uploads/products/`
- Save paths like `/uploads/products/abc123.jpg` to `Images[]`
- `MainImageUrl` will automatically return `Images[0]`
### Step 3: Verify Images Saved
```powershell
# List uploaded files
Get-ChildItem "e:\Documents\Website Projects\Sky_Art_Shop\Admin\wwwroot\uploads\products" -File | Format-Table Name, Length
# Check one product in database
$mongoPath = "C:\Program Files\MongoDB\Server\8.0\bin\mongosh.exe"
& $mongoPath --quiet --eval "use SkyArtShopCMS; db.Products.findOne({}, {name:1, images:1})"
```
You should see:
```json
{
"_id": "...",
"name": "Product Name",
"images": ["/uploads/products/abc123.jpg"]
}
```
### Step 4: Test on Static Site
1. Open: `file:///E:/Documents/Website%20Projects/Sky_Art_Shop/test-api.html`
2. Click **Load Products**
3. Images should now appear with correct URLs
### Step 5: Verify on Real Pages
After integrating the scripts (see `INTEGRATION_GUIDE.md`):
1. Open: `file:///E:/Documents/Website%20Projects/Sky_Art_Shop/shop.html`
2. Products should render with images from API
---
## 🧪 Quick API Test
Test if images are accessible:
```powershell
# Test API endpoint
Invoke-RestMethod -Uri "http://localhost:5000/api/products" | Select-Object -First 1 name, images, mainImageUrl
# Test image file serving
Invoke-WebRequest -Uri "https://localhost:5001/uploads/products/06eec547-d7e8-4d7f-876e-e282a76ae13f.jpg" -Method Head
```
---
## 🎯 Alternative: Seed Sample Product with Image
If you want to quickly test, add this to the seeding section in `Program.cs`:
```csharp
// In InitializeDatabaseAsync, after checking productRepository
var existingProducts = await productRepository.GetAllAsync();
if (!existingProducts.Any())
{
var sampleProduct = new Product
{
Name = "Sky Painting Sample",
Slug = "sky-painting-sample",
Description = "Beautiful sky artwork",
Price = 299.99m,
Category = "Paintings",
Images = new List<string>
{
"/uploads/products/06eec547-d7e8-4d7f-876e-e282a76ae13f.jpg"
}
};
await productRepository.CreateAsync(sampleProduct);
Console.WriteLine("Seeded sample product with image");
}
```
Then restart backend. The sample product will have an image path.
---
## ✅ Checklist
- [ ] Backend running on <https://localhost:5001>
- [ ] Products have images uploaded via Admin edit
- [ ] Image files exist in `wwwroot/uploads/products/`
- [ ] API returns products with `mainImageUrl` or `images[]` populated
- [ ] `test-api.html` shows images correctly
- [ ] Static pages (shop.html) render images from API
Once all checked, your CMS-powered static site is fully working! 🎉

View File

@@ -1,327 +0,0 @@
# Sky Art Shop - Admin CMS Integration Guide
## ✅ What's Been Set Up
Your Admin backend (ASP.NET Core + MongoDB) now exposes public read-only APIs for your static website:
- **Backend URL**: <https://localhost:5001> (or <http://localhost:5000>)
- **CORS Enabled**: Static files can call the API from `file://` URLs
- **Image Serving**: `/uploads/products/`, `/uploads/projects/`, `/uploads/blog/`
### Available API Endpoints
| Endpoint | Description | Example |
|----------|-------------|---------|
| `GET /api/products` | All products | Returns array of products with images, prices, descriptions |
| `GET /api/products/{id}` | Single product | Get one product by ID |
| `GET /api/projects` | Portfolio projects | Returns array of projects with images |
| `GET /api/projects/{id}` | Single project | Get one project by ID |
| `GET /api/blog` | Blog posts | Returns array of posts with featured images |
| `GET /api/blog/{id}` | Single post | Get one post by ID |
| `GET /api/pages/{slug}` | Page by slug | Get page content (e.g., "about", "contact") |
| `GET /api/categories` | Categories | All portfolio categories |
| `GET /api/settings` | Site settings | Global site configuration |
---
## 📦 Integration Files Created
All integration code has been created in your `Sky_Art_Shop` folder:
```
Sky_Art_Shop/
├── js/
│ ├── api-integration.js # Core API functions (load this first!)
│ ├── shop-page.js # For shop.html
│ ├── portfolio-page.js # For portfolio.html
│ ├── blog-page.js # For blog.html
│ ├── index-page.js # For index.html
│ └── about-page.js # For about.html
└── css/
└── api-styles.css # Styling for product/project/blog cards
```
---
## 🔧 How to Integrate Each Page
### 1. **shop.html** (Products Page)
Add these lines **before the closing `</body>` tag**:
```html
<!-- API Integration -->
<link rel="stylesheet" href="css/api-styles.css">
<script src="js/api-integration.js"></script>
<script src="js/shop-page.js"></script>
```
**In your HTML body**, add a container where products will render:
```html
<section class="products-section">
<h2>Our Products</h2>
<div id="productsContainer" class="products-grid">
<p class="loading">Loading products</p>
</div>
</section>
```
---
### 2. **portfolio.html** (Projects Page)
Add before `</body>`:
```html
<!-- API Integration -->
<link rel="stylesheet" href="css/api-styles.css">
<script src="js/api-integration.js"></script>
<script src="js/portfolio-page.js"></script>
```
Add container:
```html
<section class="portfolio-section">
<h2>Our Portfolio</h2>
<div id="projectsContainer" class="projects-grid">
<p class="loading">Loading projects</p>
</div>
</section>
```
---
### 3. **blog.html** (Blog Page)
Add before `</body>`:
```html
<!-- API Integration -->
<link rel="stylesheet" href="css/api-styles.css">
<script src="js/api-integration.js"></script>
<script src="js/blog-page.js"></script>
```
Add container:
```html
<section class="blog-section">
<h2>Latest Blog Posts</h2>
<div id="blogContainer" class="blog-grid">
<p class="loading">Loading posts</p>
</div>
</section>
```
---
### 4. **index.html** (Home Page)
Add before `</body>`:
```html
<!-- API Integration -->
<link rel="stylesheet" href="css/api-styles.css">
<script src="js/api-integration.js"></script>
<script src="js/index-page.js"></script>
```
Add containers for featured content (optional):
```html
<section class="featured-products">
<h2>Featured Products</h2>
<div id="featuredProducts" class="products-grid"></div>
</section>
<section class="recent-projects">
<h2>Recent Projects</h2>
<div id="recentProjects" class="projects-grid"></div>
</section>
<section class="recent-blog">
<h2>Latest Posts</h2>
<div id="recentBlog" class="blog-grid"></div>
</section>
```
---
### 5. **about.html** (About Page)
Add before `</body>`:
```html
<!-- API Integration -->
<link rel="stylesheet" href="css/api-styles.css">
<script src="js/api-integration.js"></script>
<script src="js/about-page.js"></script>
```
**Option A**: Load dynamic CMS content (if you create an "about" page in admin):
```html
<section class="about-section">
<div id="pageContent">
<p class="loading">Loading content</p>
</div>
</section>
```
**Option B**: Keep your static content and just load settings for site name, etc.
---
### 6. **contact.html** (Contact Page)
For contact forms, you typically keep static content. Optionally load settings:
```html
<!-- API Integration (optional) -->
<script src="js/api-integration.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const settings = await loadSettings();
// Use settings.contactEmail, settings.phone, etc. if you want
});
</script>
```
---
## 🎨 Customizing the Look
The `css/api-styles.css` file contains complete styling for:
- Product cards (`.product-card`)
- Project cards (`.project-card`)
- Blog cards (`.blog-card`)
- Grids (`.products-grid`, `.projects-grid`, `.blog-grid`)
**To match your existing design:**
1. Open `css/api-styles.css`
2. Adjust colors, fonts, spacing to match your site
3. Or copy the card styles into your existing stylesheet
---
## 🚀 Testing
1. **Start the backend** (if not already running):
```powershell
cd "e:\Documents\Website Projects\Sky_Art_Shop\Admin"
dotnet run --launch-profile https
```
2. **Open your static site**:
- `file:///E:/Documents/Website%20Projects/Sky_Art_Shop/index.html`
- `file:///E:/Documents/Website%20Projects/Sky_Art_Shop/shop.html`
- etc.
3. **Check browser console** (F12):
- Should see: `"Loaded products: X"`, `"Loaded projects: Y"`, etc.
- If you see CORS errors, they should resolve automatically (CORS is enabled)
- If API fails, make sure backend is running on <https://localhost:5001>
4. **Verify images appear**:
- Images should load from `https://localhost:5001/uploads/products/<filename>`
- Check Network tab to see image requests
---
## 🔄 How It Works
When a user visits your static site:
1. Browser loads the HTML page
2. JavaScript calls the Admin backend API (e.g., `/api/products`)
3. API queries MongoDB and returns JSON data
4. JavaScript renders the data into HTML cards with images
5. Images are served from `wwwroot/uploads/` via the backend
**When you edit content in Admin:**
- Add/edit/delete products, projects, or blog posts
- Upload images
- Changes appear immediately when users refresh the static pages
---
## 📝 Advanced: Custom Rendering
If you want custom HTML for products, edit `js/api-integration.js`:
```javascript
// In loadProducts() function, customize the template string:
return `
<article class="product-card">
<img src="${imgSrc}" alt="${product.title}">
<h3>${product.title}</h3>
<p>${product.description}</p>
<p class="price">${price}</p>
<button onclick="addToCart('${product.id}')">Add to Cart</button>
</article>`;
```
---
## 🐛 Troubleshooting
| Issue | Solution |
|-------|----------|
| **Images not showing** | 1. Check backend is running<br>2. Verify files in `Admin/wwwroot/uploads/products/`<br>3. Open DevTools Network tab, check image URLs<br>4. Confirm product has `mainImageUrl` in database |
| **"Unable to load products"** | 1. Backend not running (start with `dotnet run --launch-profile https`)<br>2. CORS issue (already fixed)<br>3. Check console for specific error |
| **Products show but no images** | Products in database missing `mainImageUrl`. Edit product in admin and upload image. |
| **Blank page** | Check console (F12) for JavaScript errors. Ensure `api-integration.js` loads first. |
| **Mixed content warning** | If static site is on `https://` but API is `http://`, change `API_BASE` in `api-integration.js` to `https://localhost:5001` |
---
## 📧 API Response Examples
### Product Response
```json
{
"id": "123abc",
"title": "Sky Painting",
"description": "Beautiful artwork...",
"price": 299.99,
"mainImageUrl": "/uploads/products/abc123.jpg",
"images": ["/uploads/products/abc123.jpg", "/uploads/products/xyz456.jpg"],
"category": "Paintings"
}
```
### Project Response
```json
{
"id": "456def",
"title": "Mural Project",
"shortDescription": "City mural...",
"coverImageUrl": "/uploads/projects/mural.jpg",
"images": ["/uploads/projects/mural.jpg"],
"category": "Murals"
}
```
---
## ✅ Next Steps
1. **Add script tags** to each HTML page as shown above
2. **Add container divs** with the correct IDs
3. **Refresh pages** and check console for "Loaded X items"
4. **Customize styles** in `api-styles.css` to match your design
5. **Test editing** content in Admin and seeing changes on static site
**Questions or issues?** Check the console first, then refer to the Troubleshooting section above.
---
**That's it!** Your static site is now powered by the Admin CMS. Any content changes you make in the admin will instantly reflect on the static pages when users refresh. 🎉

View File

@@ -1,699 +0,0 @@
# Sky Art Shop - Linux Server Migration Guide
**Migration:** Windows IIS → Linux with Nginx + Systemd
**Goal:** Zero-downtime deployments, better stability, auto-reload on file changes
---
## 🎯 Why Linux?
**Zero-downtime deployments** - Systemd can reload without dropping connections
**Better stability** - No port conflicts, no Default Web Site issues
**Auto-reload** - File changes auto-reload without manual restarts
**Lower resource usage** - More efficient than IIS
**Industry standard** - Most .NET Core production apps run on Linux
---
## 📋 Part 1: Linux Server Setup
### Recommended Options
**Option A: Local Ubuntu Server (Recommended for you)**
- Use Ubuntu 22.04 LTS Server
- Can run on same Windows PC using WSL2 or separate machine
- Free and full control
**Option B: Cloud VPS**
- DigitalOcean: $6/month
- Linode: $5/month
- Vultr: $6/month
### Step 1: Install Ubuntu Server
**If using WSL2 on Windows:**
```powershell
# In Windows PowerShell (Admin)
wsl --install -d Ubuntu-22.04
# Launch Ubuntu
wsl -d Ubuntu-22.04
```
**If using separate Linux machine:**
- Download Ubuntu 22.04 Server: <https://ubuntu.com/download/server>
- Install on dedicated machine or VM
---
## 📦 Part 2: Install Required Software on Linux
### Update System
```bash
sudo apt update && sudo apt upgrade -y
```
### Install .NET 8.0 Runtime
```bash
# Add Microsoft package repository
wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
# Install .NET Runtime and SDK
sudo apt update
sudo apt install -y dotnet-sdk-8.0 aspnetcore-runtime-8.0
# Verify installation
dotnet --version
```
### Install MongoDB on Linux
```bash
# Import MongoDB public GPG key
curl -fsSL https://pgp.mongodb.com/server-7.0.asc | \
sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor
# Add MongoDB repository
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | \
sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
# Install MongoDB
sudo apt update
sudo apt install -y mongodb-org
# Start MongoDB
sudo systemctl start mongod
sudo systemctl enable mongod
# Verify MongoDB is running
sudo systemctl status mongod
mongosh --eval "db.version()"
```
### Install Nginx (Web Server / Reverse Proxy)
```bash
sudo apt install -y nginx
# Start and enable Nginx
sudo systemctl start nginx
sudo systemctl enable nginx
# Verify Nginx is running
sudo systemctl status nginx
```
---
## 📤 Part 3: Export Data from Windows MongoDB
### On Windows
```powershell
# Create export directory
New-Item -ItemType Directory -Path "E:\mongodb_backup" -Force
# Export MongoDB database (run in PowerShell)
$mongoExport = "C:\Program Files\MongoDB\Server\8.0\bin\mongodump.exe"
if (Test-Path $mongoExport) {
& $mongoExport --db SkyArtShopDB --out "E:\mongodb_backup"
Write-Host "✅ MongoDB data exported to E:\mongodb_backup"
} else {
# Alternative: Use mongodump from command line
mongodump --db SkyArtShopDB --out "E:\mongodb_backup"
}
# Export SQLite Identity database
Copy-Item "E:\Documents\Website Projects\Sky_Art_Shop\identity.db" "E:\mongodb_backup\identity.db"
# Export uploaded images
Copy-Item "E:\Documents\Website Projects\Sky_Art_Shop\wwwroot\uploads\images\*" "E:\mongodb_backup\images\" -Recurse
Write-Host "✅ All data exported successfully"
#
# Transfer files to Ubuntu VM using SCP (PuTTY/PSCP)
# Make sure you have PSCP.exe from PuTTY installed
# Example (replace with your actual username and IP):
pscp -pw PTBelize@3030! -r "E:\mongodb_backup" PTS@192.168.10.129:/home/PTS/
```
---
## 📁 Part 4: Transfer Files to Linux
### Method 1: Using SCP (if Linux is separate machine)
```powershell
# On Windows, transfer files
scp -r "E:\mongodb_backup" username@linux-server-ip:/home/username/
scp -r "E:\Documents\Website Projects\Sky_Art_Shop" username@linux-server-ip:/home/username/skyartshop
```
### Method 2: Using WSL2 (if running on same Windows machine)
```powershell
# Files are accessible at /mnt/e/ in WSL
# In WSL terminal:
```
```bash
# Copy files to Linux home directory
cp -r /mnt/e/mongodb_backup ~/mongodb_backup
cp -r "/mnt/e/Documents/Website Projects/Sky_Art_Shop" ~/skyartshop
```
---
## 📥 Part 5: Import Data to Linux MongoDB
```bash
# Navigate to backup directory
cd ~/mongodb_backup
# Import MongoDB data
mongorestore --db SkyArtShopDB ./SkyArtShopDB
# Verify data imported
mongosh --eval "use SkyArtShopDB; db.Products.countDocuments()"
# Create MongoDB user for security (optional but recommended)
mongosh <<EOF
use admin
db.createUser({
user: "skyartshop",
pwd: "YourSecurePassword123!",
roles: [{ role: "readWrite", db: "SkyArtShopDB" }]
})
exit
EOF
```
---
## 🚀 Part 6: Deploy Application on Linux
### Create Application Directory
```bash
# Create directory for application
sudo mkdir -p /var/www/skyartshop
sudo chown -R $USER:$USER /var/www/skyartshop
# Copy application files
cp -r ~/skyartshop/* /var/www/skyartshop/
# Copy identity database
cp ~/mongodb_backup/identity.db /var/www/skyartshop/
# Create uploads directory and copy images
mkdir -p /var/www/skyartshop/wwwroot/uploads/images
cp ~/mongodb_backup/images/* /var/www/skyartshop/wwwroot/uploads/images/
# Set permissions
sudo chown -R www-data:www-data /var/www/skyartshop
sudo chmod -R 755 /var/www/skyartshop
```
### Update Connection Strings (if using MongoDB authentication)
```bash
nano /var/www/skyartshop/appsettings.json
```
Update MongoDB connection:
```json
{
"MongoDB": {
"ConnectionString": "mongodb://skyartshop:YourSecurePassword123!@localhost:27017",
"DatabaseName": "SkyArtShopDB"
}
}
```
### Publish Application for Linux
```bash
cd /var/www/skyartshop
dotnet publish SkyArtShop.csproj -c Release -o ./publish
```
---
## ⚙️ Part 7: Create Systemd Service (Auto-start & Management)
### Create Service File
```bash
sudo nano /etc/systemd/system/skyartshop.service
```
Add this content:
```ini
[Unit]
Description=Sky Art Shop ASP.NET Core Application
After=network.target
[Service]
WorkingDirectory=/var/www/skyartshop/publish
ExecStart=/usr/bin/dotnet /var/www/skyartshop/publish/SkyArtShop.dll
Restart=always
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=skyartshop
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
[Install]
WantedBy=multi-user.target
```
### Enable and Start Service
```bash
# Reload systemd
sudo systemctl daemon-reload
# Enable service to start on boot
sudo systemctl enable skyartshop
# Start the service
sudo systemctl start skyartshop
# Check status
sudo systemctl status skyartshop
# View logs
sudo journalctl -u skyartshop -f
```
---
## 🌐 Part 8: Configure Nginx as Reverse Proxy
### Create Nginx Configuration
```bash
sudo nano /etc/nginx/sites-available/skyartshop
```
Add this configuration:
```nginx
server {
listen 80;
server_name skyarts.ddns.net localhost;
location / {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
}
# Static files
location /uploads/ {
alias /var/www/skyartshop/publish/wwwroot/uploads/;
expires 30d;
add_header Cache-Control "public, immutable";
}
location /assets/ {
alias /var/www/skyartshop/publish/wwwroot/assets/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Max upload size
client_max_body_size 20M;
}
```
### Enable Site
```bash
# Create symbolic link
sudo ln -s /etc/nginx/sites-available/skyartshop /etc/nginx/sites-enabled/
# Remove default site
sudo rm /etc/nginx/sites-enabled/default
# Test configuration
sudo nginx -t
# Reload Nginx
sudo systemctl reload nginx
```
---
## 🔥 Part 9: Configure Firewall
```bash
# Allow SSH (if remote server)
sudo ufw allow 22/tcp
# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Enable firewall
sudo ufw enable
# Check status
sudo ufw status
```
---
## 🔄 Part 10: Zero-Downtime Deployment Script
Create deployment script for future updates:
```bash
nano ~/deploy.sh
```
Add this content:
```bash
#!/bin/bash
echo "🚀 Starting deployment..."
# Navigate to source directory
cd ~/skyartshop
# Pull latest changes (if using git)
# git pull origin main
# Build application
echo "📦 Building application..."
dotnet publish SkyArtShop.csproj -c Release -o /tmp/skyartshop_new
# Stop old service gracefully
echo "⏸️ Stopping service..."
sudo systemctl stop skyartshop
# Backup current version
echo "💾 Backing up current version..."
sudo mv /var/www/skyartshop/publish /var/www/skyartshop/publish_backup_$(date +%Y%m%d_%H%M%S)
# Deploy new version
echo "📤 Deploying new version..."
sudo mv /tmp/skyartshop_new /var/www/skyartshop/publish
# Ensure correct permissions
sudo chown -R www-data:www-data /var/www/skyartshop/publish
sudo chmod -R 755 /var/www/skyartshop/publish
# Start service
echo "▶️ Starting service..."
sudo systemctl start skyartshop
# Check if service started successfully
sleep 2
if sudo systemctl is-active --quiet skyartshop; then
echo "✅ Deployment successful!"
echo "🌐 Site is live at http://skyarts.ddns.net"
else
echo "❌ Service failed to start. Rolling back..."
sudo systemctl stop skyartshop
latest_backup=$(ls -t /var/www/skyartshop/ | grep publish_backup | head -1)
sudo mv /var/www/skyartshop/publish /var/www/skyartshop/publish_failed
sudo mv /var/www/skyartshop/$latest_backup /var/www/skyartshop/publish
sudo systemctl start skyartshop
echo "⚠️ Rolled back to previous version"
fi
# View logs
sudo journalctl -u skyartshop -n 20
```
Make script executable:
```bash
chmod +x ~/deploy.sh
```
---
## 🌐 Part 11: Network Configuration
### Update Router Port Forwarding
- Forward port 80 (HTTP) to your **Linux server's IP** (not Windows anymore)
- If WSL2, forward to Windows IP (WSL2 uses NAT)
### Update No-IP DUC
- Install No-IP DUC on Linux:
```bash
cd /usr/local/src
sudo wget http://www.noip.com/client/linux/noip-duc-linux.tar.gz
sudo tar xzf noip-duc-linux.tar.gz
cd noip-2.1.9-1
sudo make
sudo make install
# Configure
sudo noip2 -C
# Start service
sudo noip2
```
---
## 🔒 Part 12: Add HTTPS (Optional but Recommended)
```bash
# Install Certbot
sudo apt install -y certbot python3-certbot-nginx
# Get SSL certificate
sudo certbot --nginx -d skyarts.ddns.net
# Auto-renewal is configured automatically
sudo certbot renew --dry-run
```
---
## 📊 Part 13: Monitoring & Management
### Useful Commands
```bash
# View application logs
sudo journalctl -u skyartshop -f
# Restart application
sudo systemctl restart skyartshop
# Stop application
sudo systemctl stop skyartshop
# Start application
sudo systemctl start skyartshop
# Check application status
sudo systemctl status skyartshop
# View Nginx logs
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log
# Check MongoDB status
sudo systemctl status mongod
# Check disk space
df -h
# Check memory usage
free -h
# Check CPU usage
top
```
---
## 🔄 Daily Workflow: Making Changes
### Option 1: Edit Locally, Deploy
```bash
# 1. Edit files on Windows in VS Code
# 2. Transfer to Linux
scp -r "E:\Documents\Website Projects\Sky_Art_Shop\*" username@linux:/home/username/skyartshop/
# 3. Run deployment script
ssh username@linux
./deploy.sh
```
### Option 2: Edit Directly on Linux (Recommended)
```bash
# SSH into Linux server
ssh username@linux
# Navigate to source
cd ~/skyartshop
# Edit files with nano or vim
nano Views/Shared/_AdminLayout.cshtml
# Deploy changes
./deploy.sh
```
### Option 3: Use Git (Best Practice)
```bash
# On Windows, commit changes
git add .
git commit -m "Updated admin sidebar"
git push origin main
# On Linux, pull and deploy
ssh username@linux
cd ~/skyartshop
git pull origin main
./deploy.sh
```
---
## 🎯 Advantages Over Windows/IIS
| Feature | Windows/IIS | Linux/Nginx |
|---------|-------------|-------------|
| **Zero-downtime** | ❌ Requires restart | ✅ Seamless reload |
| **Port conflicts** | ❌ Common issue | ✅ No conflicts |
| **Resource usage** | ⚠️ Higher | ✅ Lower |
| **Deployment** | ⚠️ Manual, downtime | ✅ Scripted, automated |
| **File changes** | ⚠️ Requires restart | ✅ Auto-reload |
| **Stability** | ⚠️ Default site issues | ✅ Very stable |
| **Cost** | 💰 Windows Server license | ✅ Free |
| **Performance** | ⚠️ Good | ✅ Excellent |
---
## 🆘 Troubleshooting
### Service won't start
```bash
# Check logs
sudo journalctl -u skyartshop -n 50
# Check if port 5000 is available
sudo netstat -tulpn | grep 5000
# Test application manually
cd /var/www/skyartshop/publish
dotnet SkyArtShop.dll
```
### MongoDB connection issues
```bash
# Check MongoDB is running
sudo systemctl status mongod
# Check connection
mongosh --eval "db.adminCommand('ping')"
# View MongoDB logs
sudo tail -f /var/log/mongodb/mongod.log
```
### Nginx issues
```bash
# Check Nginx configuration
sudo nginx -t
# View error logs
sudo tail -f /var/log/nginx/error.log
# Restart Nginx
sudo systemctl restart nginx
```
---
## 📝 Migration Checklist
- [ ] Ubuntu Server 22.04 installed
- [ ] .NET 8.0 SDK/Runtime installed
- [ ] MongoDB installed and running
- [ ] Nginx installed and configured
- [ ] MongoDB data exported from Windows
- [ ] SQLite identity.db copied
- [ ] Images copied to Linux
- [ ] MongoDB data imported to Linux
- [ ] Application deployed to /var/www/skyartshop
- [ ] Systemd service created and running
- [ ] Nginx reverse proxy configured
- [ ] Firewall configured
- [ ] Router port forwarding updated
- [ ] No-IP DUC configured on Linux
- [ ] Site accessible at skyarts.ddns.net
- [ ] Admin login working
- [ ] Images displaying correctly
- [ ] Deployment script tested
- [ ] HTTPS configured (optional)
---
## 🎉 Success
Once completed, your site will be:
- ✅ Running on stable Linux infrastructure
- ✅ Auto-reloading on file changes
- ✅ Zero-downtime deployments
- ✅ Better performance
- ✅ No more IIS headaches!
**Your site:** <http://skyarts.ddns.net>
**Admin panel:** <http://skyarts.ddns.net/admin>
---
## 📞 Need Help?
Common resources:
- ASP.NET Core on Linux: <https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx>
- Ubuntu Server Guide: <https://ubuntu.com/server/docs>
- MongoDB on Ubuntu: <https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-ubuntu/>
- Nginx Documentation: <https://nginx.org/en/docs/>
---
**Ready to migrate? Start with Part 1!** 🚀

View File

@@ -1,273 +0,0 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System;
using System.ComponentModel.DataAnnotations;
namespace SkyArtShop.Models
{
public class Page
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[Required]
public string PageName { get; set; } = string.Empty;
public string PageSlug { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Subtitle { get; set; } = string.Empty;
public string HeroImage { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string MetaDescription { get; set; } = string.Empty;
public List<string> ImageGallery { get; set; } = new List<string>(); // Right sidebar images
public List<TeamMember> TeamMembers { get; set; } = new List<TeamMember>(); // Team section
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class TeamMember
{
public string Name { get; set; } = string.Empty;
public string Role { get; set; } = string.Empty;
public string Bio { get; set; } = string.Empty;
public string PhotoUrl { get; set; } = string.Empty;
}
public class PortfolioCategory
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[Required]
public string Name { get; set; } = string.Empty;
[Required]
public string Slug { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string ThumbnailImage { get; set; } = string.Empty;
public string FeaturedImage { get; set; } = string.Empty;
public int DisplayOrder { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class PortfolioProject
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[Required]
public string Title { get; set; } = string.Empty;
[Required]
public string CategoryId { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string FeaturedImage { get; set; } = string.Empty;
public List<string> Images { get; set; } = new List<string>();
public string ProjectDate { get; set; } = string.Empty;
public int DisplayOrder { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class Product
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[Required]
public string Name { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public string SKU { get; set; } = string.Empty;
public string ShortDescription { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
[Required]
public decimal Price { get; set; }
public string Category { get; set; } = string.Empty;
public string Color { get; set; } = string.Empty; // Legacy single color (kept for backward compatibility)
public List<string> Colors { get; set; } = new List<string>(); // Multiple colors
// Primary image used in listings/forms
public string ImageUrl { get; set; } = string.Empty;
public List<string> Images { get; set; } = new List<string>();
public bool IsFeatured { get; set; }
public bool IsTopSeller { get; set; }
public int StockQuantity { get; set; }
public bool IsActive { get; set; } = true;
// Sales Tracking
public int UnitsSold { get; set; } = 0;
public decimal TotalRevenue { get; set; } = 0;
public double AverageRating { get; set; } = 0;
public int TotalReviews { get; set; } = 0;
public decimal CostPrice { get; set; } = 0; // For profit margin calculation
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class Order
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
public string CustomerEmail { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public List<OrderItem> Items { get; set; } = new List<OrderItem>();
public decimal TotalAmount { get; set; }
public string Status { get; set; } = "Pending"; // Pending, Completed, Cancelled
public DateTime OrderDate { get; set; } = DateTime.UtcNow;
public DateTime? CompletedDate { get; set; }
}
public class OrderItem
{
public string ProductId { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
public string SKU { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal Subtotal { get; set; }
}
public class ProductView
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
public string ProductId { get; set; } = string.Empty;
public string SessionId { get; set; } = string.Empty;
public string IpAddress { get; set; } = string.Empty;
public DateTime ViewedAt { get; set; } = DateTime.UtcNow;
}
public class BlogPost
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[Required]
public string Title { get; set; } = string.Empty;
[Required]
public string Slug { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string Excerpt { get; set; } = string.Empty;
public string FeaturedImage { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public List<string> Tags { get; set; } = new List<string>();
public bool IsPublished { get; set; } = true;
public DateTime PublishedDate { get; set; } = DateTime.UtcNow;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class MenuItem
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[Required]
public string Label { get; set; } = string.Empty;
[Required]
public string Url { get; set; } = string.Empty;
public int DisplayOrder { get; set; }
public bool IsActive { get; set; } = true;
public bool ShowInNavbar { get; set; } = true;
public bool ShowInDropdown { get; set; } = true;
public bool OpenInNewTab { get; set; } = false;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
[BsonIgnoreExtraElements]
public class SiteSettings
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
public string SiteName { get; set; } = "Sky Art Shop";
public string SiteTagline { get; set; } = "Scrapbooking and Journaling Fun";
public string ContactEmail { get; set; } = "info@skyartshop.com";
public string ContactPhone { get; set; } = "+501 608-0409";
public string InstagramUrl { get; set; } = "#";
public string FooterText { get; set; } = "© 2035 by Sky Art Shop. Powered and secured by Wix";
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class HomepageSection
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
public string SectionType { get; set; } = string.Empty; // hero, inspiration, collection, promotion
public string Title { get; set; } = string.Empty;
public string Subtitle { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string ImageUrl { get; set; } = string.Empty;
public string ButtonText { get; set; } = string.Empty;
public string ButtonUrl { get; set; } = string.Empty;
public int DisplayOrder { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
// Additional properties for specific section types
public Dictionary<string, string> AdditionalData { get; set; } = new Dictionary<string, string>();
}
public class CollectionItem
{
public string Title { get; set; } = string.Empty;
public string ImageUrl { get; set; } = string.Empty;
public string Link { get; set; } = string.Empty;
}
public class PromotionCard
{
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string ButtonText { get; set; } = string.Empty;
public string ButtonUrl { get; set; } = string.Empty;
public bool IsFeatured { get; set; }
}
public class AdminUser
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
public string PasswordHash { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime LastLogin { get; set; }
}
}

View File

@@ -1,384 +0,0 @@
# Sky Art Shop - System Optimization & Synchronization Report
**Date**: December 1, 2025
**Status**: ✅ **FULLY OPTIMIZED & SYNCHRONIZED**
---
## 📊 Executive Summary
Complete front-end and back-end synchronization achieved. All dynamic content from the admin panel is properly reflected on the website. Code optimized, duplicates removed, and performance improved.
---
## ✅ Completed Optimizations
### 1. Back-End to Front-End Synchronization ✅
**All Content Types Synced:**
-**Products** - Admin create/edit instantly reflects on Shop page
-**Blog Posts** - Admin changes appear immediately on Blog listing/detail pages
-**Portfolio** - Categories and projects sync to Portfolio views
-**Pages** - About and custom pages editable from admin
-**Navigation Menu** - Fully editable with instant front-end updates
-**Site Settings** - Global settings (hero, footer, contact) sync across all pages
**CRUD Operations Verified:**
- ✅ CREATE - All forms validated, slugs auto-generated, data saved to MongoDB
- ✅ READ - All public views load from database dynamically
- ✅ UPDATE - Edit forms pre-populate, changes save correctly
- ✅ DELETE - Soft delete (IsActive flags) or hard delete with confirmation
---
### 2. Database Structure ✅
**MongoDB Collections Verified:**
```
✅ Products - 13 fields (Name, Slug, Price, Category, Images, IsTopSeller, etc.)
✅ BlogPosts - 10 fields (Title, Slug, Content, Excerpt, Tags, IsPublished, etc.)
✅ PortfolioCategories - 8 fields (Name, Slug, Images, DisplayOrder, etc.)
✅ PortfolioProjects - 9 fields (Title, CategoryId, Images, DisplayOrder, etc.)
✅ Pages - 9 fields (PageName, PageSlug, Title, Content, HeroImage, etc.)
✅ MenuItems - 7 fields (Label, Url, DisplayOrder, IsActive, OpenInNewTab, etc.)
✅ SiteSettings - 11 fields (SiteName, HeroTitle, ContactEmail, FooterText, etc.)
```
**Key Improvements:**
- All field names match between models and database
- No orphaned fields or outdated columns
- All relationships properly structured (CategoryId for projects)
- Timestamps (CreatedAt, UpdatedAt) on all content types
---
### 3. Code Optimization ✅
**Front-End Improvements:**
1. **JavaScript Consolidation**
- Created `cart.js` - Centralized shopping cart functions
- Created `admin.js` - Shared admin panel utilities
- Removed duplicate `addToCart()` functions from 2 views
- Removed duplicate upload/delete functions
2. **Performance Enhancements**
- Added `loading="lazy"` to all product and content images
- Lazy loading reduces initial page load by ~40%
- Images load as user scrolls, improving perceived performance
3. **CSS/JS Organization**
- No inline styles found (all in main.css)
- Proper separation: main.css (public), admin layout (Bootstrap)
- Scripts loaded at end of body for non-blocking render
4. **Removed Duplicate Files**
- Archived 13 static HTML files (replaced by dynamic Razor views)
- Moved to `_archive_static_html/` folder:
- index.html, shop.html, blog.html, about.html, contact.html
- portfolio.html + 4 portfolio category pages
- test-api.html, shop-demo.html, SHOP_HTML_INTEGRATION.html
**Back-End Improvements:**
1. **Controller Optimization**
- All controllers use dependency injection (MongoDBService, SlugService)
- Centralized SlugService for URL-friendly slug generation
- ModelState validation on all POST actions
- Consistent error handling with TempData messages
2. **Service Layer**
- MongoDBService: Generic CRUD methods (no duplication)
- SlugService: Regex-based slug generation (one implementation)
- ImageUploadService: Centralized in AdminUploadController
3. **Security Improvements**
- All admin routes protected with `[Authorize(Roles="Admin")]`
- File upload validation (extensions, size limits)
- SQL injection prevention (parameterized MongoDB queries)
- XSS prevention (Razor auto-escapes output)
---
### 4. CMS Editing Capabilities ✅
**Client Can Edit Everything Without Touching Code:**
| Content Area | Editable Fields | Admin Page |
|-------------|----------------|-----------|
| **Home Page** | Hero title, subtitle, button text/URL | Site Settings |
| **About Page** | Full content (currently static, can be made dynamic) | Pages > About |
| **Products** | Name, description, price, category, images, stock | Products |
| **Portfolio** | Categories (name, images), Projects (title, images, category) | Portfolio |
| **Blog** | Title, content, excerpt, featured image, tags, publish status | Blog |
| **Navigation** | Menu items (label, URL, order, visibility) | Navigation Menu |
| **Contact Info** | Email, phone, social media links | Site Settings |
| **Footer** | Footer text, copyright | Site Settings |
**Form Features:**
- ✅ User-friendly admin interface with Bootstrap 5
- ✅ Text editors (standard textareas - TinyMCE removed for simplicity)
- ✅ Image upload with preview
- ✅ Drag-and-drop ordering (via DisplayOrder fields)
- ✅ Active/Inactive toggles
- ✅ Validation with error messages
- ✅ Success notifications
---
### 5. Performance Improvements ✅
**Metrics:**
-**Page Load Time**: Optimized with lazy image loading
-**Image Optimization**: Lazy loading on all product/portfolio images
-**CSS/JS**: Minified Bootstrap CDN, local scripts optimized
-**Database Queries**: Efficient MongoDB queries with filters
-**Caching**: ASP.NET Core response caching ready (can add [ResponseCache] attributes)
**Specific Optimizations:**
1. Lazy loading images: `loading="lazy"` attribute
2. No blocking scripts: JS loaded at document end
3. Efficient queries: Only fetch active/published content
4. Reduced duplication: Shared cart.js and admin.js
---
### 6. Code Cleanup ✅
**Files Removed/Archived:**
- ✅ 13 static HTML files moved to `_archive_static_html/`
- ✅ Duplicate JavaScript functions consolidated
- ✅ Unused TinyMCE integration removed
**Code Quality:**
- ✅ Consistent naming conventions (PascalCase for C#, camelCase for JS)
- ✅ Proper indentation and formatting
- ✅ No dead code or unused functions
- ✅ Reusable components (NavigationViewComponent, shared layouts)
- ✅ DRY principle applied (no duplicate CRUD logic)
---
## 🧪 System Testing Results
### CRUD Operations Testing
| Operation | Product | Blog | Portfolio | Pages | Result |
|-----------|---------|------|-----------|-------|--------|
| **Create** | ✅ | ✅ | ✅ | ✅ | Pass |
| **Read** | ✅ | ✅ | ✅ | ✅ | Pass |
| **Update** | ✅ | ✅ | ✅ | ✅ | Pass |
| **Delete** | ✅ | ✅ | ✅ | ✅ | Pass |
**Test Details:**
- Create: Forms validate, slugs auto-generate, save to database
- Read: Public pages load from database, no hardcoded content
- Update: Edit forms pre-populate, changes save correctly
- Delete: Confirmation prompts, removes from database, updates front-end
### Responsive Design Testing
| Device | Resolution | Navigation | Images | Forms | Result |
|--------|-----------|------------|--------|-------|--------|
| Desktop | 1920x1080 | ✅ | ✅ | ✅ | Pass |
| Tablet | 768x1024 | ✅ | ✅ | ✅ | Pass |
| Mobile | 375x667 | ✅ | ✅ | ✅ | Pass |
**Features Tested:**
- ✅ Hamburger menu on mobile
- ✅ Responsive grid layouts
- ✅ Touch-friendly buttons
- ✅ Readable text on small screens
### Navigation & Links
- ✅ All menu items load correctly
- ✅ Anchor links work (Top Sellers, Promotion)
- ✅ Admin links function properly
- ✅ External links (Instagram) work
- ✅ Breadcrumbs on Portfolio category pages
### Forms
- ✅ Contact form (ready for email integration)
- ✅ Product add to cart
- ✅ Admin login
- ✅ All admin CRUD forms
- ✅ Image upload forms
---
## 📁 File Structure
```
Sky_Art_Shop/
├── Controllers/
│ ├── AdminBlogController.cs ✅ Optimized
│ ├── AdminController.cs ✅ Optimized
│ ├── AdminMenuController.cs ✅ NEW
│ ├── AdminPagesController.cs ✅ Optimized
│ ├── AdminPortfolioController.cs ✅ Optimized
│ ├── AdminProductsController.cs ✅ Optimized
│ ├── AdminSettingsController.cs ✅ Optimized
│ ├── AdminUploadController.cs ✅ Optimized + Index view
│ ├── AboutController.cs
│ ├── BlogController.cs
│ ├── ContactController.cs
│ ├── HomeController.cs ✅ Optimized
│ ├── PortfolioController.cs
│ └── ShopController.cs
├── Models/
│ └── DatabaseModels.cs ✅ Clean, validated
├── Services/
│ ├── MongoDBService.cs ✅ Generic CRUD
│ └── SlugService.cs ✅ NEW - Centralized
├── Views/
│ ├── Home/Index.cshtml ✅ Optimized (lazy loading)
│ ├── Shop/Index.cshtml ✅ Optimized (lazy loading)
│ ├── Blog/Index.cshtml, Post.cshtml
│ ├── Portfolio/Index.cshtml, Category.cshtml
│ ├── About/Index.cshtml ✅ Static content (can be made dynamic)
│ ├── Contact/Index.cshtml
│ ├── Admin*.cshtml (10 files) ✅ All functional
│ └── Shared/
│ ├── _Layout.cshtml ✅ + cart.js
│ └── _AdminLayout.cshtml ✅ + admin.js
├── wwwroot/
│ ├── assets/
│ │ ├── css/main.css ✅ Clean, organized
│ │ └── js/
│ │ ├── main.js ✅ Navigation, utilities
│ │ ├── cart.js ✅ NEW - Shopping cart
│ │ └── admin.js ✅ NEW - Admin utilities
│ └── uploads/images/ ✅ User uploads
└── _archive_static_html/ ✅ OLD HTML files
```
---
## 🎯 Final Deliverables
### ✅ Fully Synced Website
- Front-end dynamically renders all database content
- Admin changes appear instantly on public site
- No hardcoded content (except About page structure)
### ✅ Clean Codebase
- No duplicate code
- Shared JavaScript functions (cart.js, admin.js)
- Consistent coding standards
- Proper separation of concerns
### ✅ Fully Functioning CMS
- Complete admin panel for all content types
- Menu management
- Media upload library
- Site settings editor
- User-friendly forms with validation
### ✅ Performance Optimized
- Lazy loading images
- Efficient database queries
- Consolidated JavaScript
- No blocking resources
### ✅ Mobile Responsive
- Works on all screen sizes
- Touch-friendly interface
- Responsive navigation
---
## 🚀 How to Use
### Admin Panel Access
1. **URL**: <http://localhost:5000/admin/login>
2. **Credentials**: <admin@skyartshop.com> / Admin123!
### Admin Features
- **Dashboard**: Overview and quick links
- **Pages**: Manage custom pages
- **Blog**: Create/edit blog posts
- **Portfolio**: Manage categories and projects
- **Products**: Shop inventory management
- **Navigation Menu**: Full control over site navigation
- **Site Settings**: Global site configuration
- **Media Upload**: Image library with upload/delete
### Reseed Navigation Menu
1. Go to **Navigation Menu** in admin
2. Click **"Reseed Menu"** button
3. Confirms: Adds all 10 menu items (Home, Shop, Top Sellers, Promotion, Portfolio, Blog, About, Instagram, Contact, My Wishlist)
---
## 🔧 Technical Stack
- **Framework**: ASP.NET Core 8.0 MVC
- **Database**: MongoDB (content) + SQLite (authentication)
- **Authentication**: ASP.NET Core Identity
- **Front-End**: Bootstrap 5, Custom CSS, Vanilla JavaScript
- **Architecture**: MVC with Service Layer pattern
- **Validation**: Server-side with ModelState
- **Security**: Role-based authorization, input sanitization
---
## 📊 Performance Metrics
- **Build Time**: ~4 seconds
- **Startup Time**: ~2 seconds
- **Page Load (Home)**: Fast with lazy loading
- **Database Queries**: Optimized with filters
- **Code Quality**: Zero build errors/warnings
---
## 🎨 Next Steps (Optional Enhancements)
1. **Email Integration**: Connect contact form to SMTP service
2. **Shopping Cart Checkout**: Add payment processing
3. **Image Optimization**: Auto-resize/compress on upload
4. **SEO**: Add meta tags, sitemaps, structured data
5. **Analytics**: Google Analytics integration
6. **Caching**: Add response caching for better performance
7. **About Page**: Make content editable from admin (currently static)
8. **Search**: Add product/blog search functionality
---
## ✅ System Status: PRODUCTION READY
All objectives completed. The website is fully functional, optimized, and ready for client use.
**Build Status**: ✅ Success (0 errors, 0 warnings)
**Application Status**: ✅ Running on <http://localhost:5000>
**Database**: ✅ Synced and validated
**Code Quality**: ✅ Clean and optimized
**Performance**: ✅ Optimized with lazy loading
**CMS**: ✅ Fully functional admin panel
---
**Report Generated**: December 1, 2025
**Project**: Sky Art Shop CMS
**Version**: 1.0 - Production Ready

View File

@@ -1,350 +0,0 @@
# 🎉 Sky Art Shop Website - Project Complete
## ✅ What Has Been Built
Congratulations! Your complete Sky Art Shop website is ready. Here's what you have:
### 📄 10 Fully Functional Pages
1.**Home Page** - Beautiful landing page with hero, products, and promotions
2. 🎨 **Portfolio Page** - Showcase your creative work
3. 🛍️ **Shop Page** - Full product catalog with filtering
4. **About Page** - Tell your story
5. 📧 **Contact Page** - Contact form and information
6. 📝 **Blog Page** - Share ideas and tutorials
7. 📸 **Displays Portfolio** - Display projects category
8. 🎭 **Personal Crafts Portfolio** - Personal projects category
9. 💌 **Card Making Portfolio** - Card making category
10. 📚 **Scrapbook Albums Portfolio** - Scrapbook category
### 🎨 Professional Design Features
- ✅ Modern, clean aesthetic
- ✅ Responsive design (desktop, tablet, mobile)
- ✅ Purple and pink color scheme (customizable)
- ✅ Smooth animations and transitions
- ✅ Professional typography
- ✅ Consistent branding throughout
### 💻 Technical Features
- ✅ Mobile hamburger menu
- ✅ Smooth scrolling
- ✅ Product filtering by category
- ✅ Product sorting by price
- ✅ Contact form with validation
- ✅ Add to cart notifications
- ✅ Scroll-to-top button
- ✅ Hover effects on images
- ✅ Active page highlighting
- ✅ Cross-browser compatible
### 📁 Well-Organized Structure
```
Sky_Art_Shop/
├── 6 Main HTML pages
├── 4 Portfolio category pages
├── Comprehensive CSS (900+ lines)
├── Interactive JavaScript (300+ lines)
├── Complete documentation (4 guide files)
└── Organized folder structure
```
## 🚀 Next Steps to Launch
### Step 1: Add Your Images (Required)
- Read `IMAGE-GUIDE.md` for complete list
- Total images needed: 59
- Organize in `assets/images/` folder
- Or use placeholders temporarily
### Step 2: Customize Content
- [ ] Update business information in Contact page
- [ ] Add your products to Shop page
- [ ] Write your About page story
- [ ] Add portfolio project images
- [ ] Customize colors in CSS
### Step 3: Test Everything
- [ ] Open in web browser
- [ ] Test on mobile device
- [ ] Click all navigation links
- [ ] Test contact form
- [ ] Try product filters
- [ ] Check responsive design
### Step 4: Go Live
Choose a hosting option:
- **GitHub Pages** (Free)
- **Netlify** (Free)
- **Traditional hosting** (Bluehost, HostGator, etc.)
## 📚 Documentation Provided
### 1. README.md
Complete project overview, features, and technical details
### 2. SETUP-GUIDE.md
Quick start guide to get you up and running
### 3. IMAGE-GUIDE.md
Detailed list of all 59 images needed with specifications
### 4. SITEMAP.md
Complete site structure and navigation map
### 5. PROJECT-SUMMARY.md
This file - your project completion checklist
## 🎯 What Makes This Website Special
### For Visitors
- **Easy Navigation**: Clear menu structure
- **Mobile-Friendly**: Works on all devices
- **Fast Loading**: Optimized code
- **Professional Look**: Modern design
- **Easy Contact**: Simple contact form
### For You (Site Owner)
- **Easy to Update**: Well-commented HTML
- **Customizable**: Change colors, fonts easily
- **Well-Documented**: Complete guides included
- **Expandable**: Easy to add features
- **SEO-Ready**: Proper HTML structure
## 💡 Customization Quick Reference
### Change Colors
Edit `assets/css/main.css` line 10-12:
```css
--primary-color: #6B4E9B;
--secondary-color: #E91E63;
--accent-color: #FF9800;
```
### Add Products
Copy product card in `shop.html` around line 75
### Update Contact Info
Edit `contact.html` around line 60
### Modify Navigation
Update nav menu in each HTML file (around line 22)
## 📊 Website Statistics
- **Total Files**: 10 HTML + 1 CSS + 1 JS = 12 code files
- **Total Folders**: 8 organized folders
- **Code Lines**: ~3,000+ lines of HTML, CSS, JavaScript
- **Documentation**: 4 comprehensive guide files
- **Responsive Breakpoints**: 2 (tablet 768px, mobile 480px)
- **Color Variables**: 11 customizable colors
- **JavaScript Functions**: 15+ interactive features
## 🎨 Design Specifications
### Colors Used
- Primary (Purple): `#6B4E9B`
- Secondary (Pink): `#E91E63`
- Accent (Orange): `#FF9800`
- Background: `#FFFFFF`
- Text: `#333333`
- Light Background: `#F5F5F5`
### Fonts
- Body: Segoe UI (system font)
- Headings: Georgia (serif)
### Spacing Scale
- XS: 0.5rem (8px)
- SM: 1rem (16px)
- MD: 2rem (32px)
- LG: 3rem (48px)
- XL: 4rem (64px)
## 🌟 Key Features Highlight
### 1. Responsive Navigation
- Desktop: Full horizontal menu
- Mobile: Hamburger menu with slide-in
- Active page highlighting
### 2. Portfolio System
- Main gallery page
- 4 category pages
- Ready for project detail pages
- Breadcrumb navigation
### 3. Shop Functionality
- Product grid layout
- Category filtering
- Price sorting
- Add to cart (ready for backend)
### 4. Contact System
- Form validation
- Success/error messages
- Business information display
- Social media links
### 5. Professional Touches
- Smooth scrolling
- Hover animations
- Loading transitions
- Scroll-to-top button
- Image lazy loading
## ✨ Browser Support
Tested and works on:
- ✅ Chrome (latest)
- ✅ Firefox (latest)
- ✅ Safari (latest)
- ✅ Edge (latest)
- ✅ Mobile browsers (iOS Safari, Chrome Mobile)
## 🔧 Technologies Used
### Frontend
- **HTML5**: Semantic markup
- **CSS3**: Modern styling (Grid, Flexbox, Variables)
- **JavaScript ES6+**: Interactive features
### No Dependencies
- No jQuery required
- No Bootstrap needed
- No external libraries
- Pure vanilla code = Fast loading
## 📈 Performance Features
- Optimized CSS (single file)
- Efficient JavaScript (single file)
- Image lazy loading ready
- Minimal HTTP requests
- Mobile-first responsive design
## 🎓 Learning Resources
If you want to customize further:
### HTML & CSS
- [MDN Web Docs](https://developer.mozilla.org/)
- [CSS-Tricks](https://css-tricks.com/)
### JavaScript
- [JavaScript.info](https://javascript.info/)
- [MDN JavaScript Guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide)
### Responsive Design
- [A Complete Guide to Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/)
- [A Complete Guide to Grid](https://css-tricks.com/snippets/css/complete-guide-grid/)
## 🎉 You're All Set
Your Sky Art Shop website is:
- ✅ Fully functional
- ✅ Professionally designed
- ✅ Mobile responsive
- ✅ Well documented
- ✅ Ready to customize
- ✅ Ready to launch
### Quick Start Command
1. Open Visual Studio Code
2. Right-click `index.html`
3. Select "Open with Live Server"
4. Start customizing!
## 💬 Final Notes
### What's Included
✅ Complete website structure
✅ All pages connected
✅ Responsive design
✅ Interactive features
✅ Professional styling
✅ Comprehensive documentation
### What You Need to Add
📸 Your actual images (59 total)
✍️ Your content and products
🎨 Your branding (optional color changes)
🌐 Web hosting (when ready to go live)
### Future Enhancements (Optional)
- Shopping cart backend
- Payment integration
- User accounts
- Blog CMS
- Product search
- Reviews system
- Newsletter signup
- Social media feeds
## 🙏 Thank You
Your Sky Art Shop website has been built with care and attention to detail. Every feature has been thoughtfully implemented to provide the best experience for your customers while remaining easy for you to manage and customize.
**Remember**: Start simple, test often, and customize gradually. The website is ready to use right now, and you can enhance it as you grow!
---
## 📞 Quick Reference
**Project Location**: `E:\Documents\Website Projects\Sky_Art_Shop`
**To View**: Open `index.html` in browser or use Live Server
**To Edit**: Open any file in Visual Studio Code
**To Deploy**: Follow SETUP-GUIDE.md deployment section
---
**Built on**: December 1, 2025
**Status**: ✅ Complete and Ready
**Next Step**: Add images and customize content
🎨 **Happy Crafting with Your New Website!**
---
*If you have questions, refer to the documentation files or search online for HTML/CSS/JavaScript tutorials.*

View File

@@ -1,263 +0,0 @@
using SkyArtShop.Services;
using SkyArtShop.Models;
using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
var builder = WebApplication.CreateBuilder(args);
// Configure URLs based on environment
if (builder.Environment.IsProduction())
{
// Production: Listen on all interfaces, port 80 and 443
builder.WebHost.UseUrls("http://*:80");
}
else
{
// Development: Listen only on localhost, port 5001
builder.WebHost.UseUrls("http://localhost:5001");
}
// Add services to the container
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
// Add EF Core (SQLite) for Identity (separate from MongoDB content storage)
var identityConnection = builder.Configuration.GetConnectionString("IdentityConnection") ?? "Data Source=identity.db";
builder.Services.AddDbContext<SkyArtShop.Data.ApplicationDbContext>(options =>
options.UseSqlite(identityConnection));
// Add Identity
builder.Services.AddIdentity<SkyArtShop.Data.ApplicationUser, IdentityRole>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredLength = 6;
})
.AddEntityFrameworkStores<SkyArtShop.Data.ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/admin/login";
options.AccessDeniedPath = "/admin/login";
});
// Configure MongoDB
builder.Services.Configure<MongoDBSettings>(
builder.Configuration.GetSection("MongoDB"));
builder.Services.AddSingleton<MongoDBService>();
builder.Services.AddSingleton<SlugService>();
// Add session support
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(2);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
// Add HttpContextAccessor
builder.Services.AddHttpContextAccessor();
var app = builder.Build();
// Initialize database with default data
await InitializeDatabase(app.Services);
// Configure the HTTP request pipeline
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
// app.UseHttpsRedirection(); // Disabled for HTTP-only development
app.UseStaticFiles();
app.UseRouting();
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
async Task InitializeDatabase(IServiceProvider services)
{
using var scope = services.CreateScope();
var mongoService = scope.ServiceProvider.GetRequiredService<MongoDBService>();
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
// Apply Identity migrations and seed admin user/role
var dbContext = scope.ServiceProvider.GetRequiredService<SkyArtShop.Data.ApplicationDbContext>();
await dbContext.Database.EnsureCreatedAsync();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<SkyArtShop.Data.ApplicationUser>>();
const string adminRole = "Admin";
if (!await roleManager.RoleExistsAsync(adminRole))
{
await roleManager.CreateAsync(new IdentityRole(adminRole));
}
var adminEmail = configuration["AdminUser:Email"] ?? "admin@skyartshop.com";
var existingAdmin = await userManager.FindByEmailAsync(adminEmail);
if (existingAdmin == null)
{
var adminUser = new SkyArtShop.Data.ApplicationUser
{
UserName = adminEmail,
Email = adminEmail,
DisplayName = configuration["AdminUser:Name"] ?? "Admin"
};
var createResult = await userManager.CreateAsync(adminUser, configuration["AdminUser:Password"] ?? "Admin123!");
if (createResult.Succeeded)
{
await userManager.AddToRoleAsync(adminUser, adminRole);
}
}
// Initialize site settings
var settings = await mongoService.GetAllAsync<SiteSettings>("SiteSettings");
if (!settings.Any())
{
var defaultSettings = new SiteSettings
{
SiteName = "Sky Art Shop",
SiteTagline = "Scrapbooking and Journaling Fun",
ContactEmail = "info@skyartshop.com",
ContactPhone = "+501 608-0409",
FooterText = "© 2035 by Sky Art Shop. Powered and secured by Wix",
UpdatedAt = DateTime.UtcNow
};
await mongoService.InsertAsync("SiteSettings", defaultSettings);
}
// Initialize default portfolio categories
var categories = await mongoService.GetAllAsync<PortfolioCategory>("PortfolioCategories");
if (!categories.Any())
{
var defaultCategories = new[]
{
new PortfolioCategory { Name = "Displays", Slug = "displays", Description = "Creative display projects showcasing our work", DisplayOrder = 1, IsActive = true },
new PortfolioCategory { Name = "Personal Craft Projects", Slug = "personal-craft-projects", Description = "Personal creative projects and handmade crafts", DisplayOrder = 2, IsActive = true },
new PortfolioCategory { Name = "Card Making Projects", Slug = "card-making", Description = "Handmade cards for every occasion", DisplayOrder = 3, IsActive = true },
new PortfolioCategory { Name = "Scrapbook Albums", Slug = "scrapbook-albums", Description = "Preserving memories through creative scrapbooking", DisplayOrder = 4, IsActive = true }
};
foreach (var category in defaultCategories)
{
await mongoService.InsertAsync("PortfolioCategories", category);
}
}
// Initialize About page
var pages = await mongoService.GetAllAsync<Page>("Pages");
if (!pages.Any(p => p.PageSlug == "about"))
{
var aboutPage = new Page
{
PageName = "About",
PageSlug = "about",
Title = "About Sky Art Shop",
Subtitle = "Creating moments, one craft at a time",
Content = @"
<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>
<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>",
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
await mongoService.InsertAsync("Pages", aboutPage);
}
// Initialize menu items
var menuItems = await mongoService.GetAllAsync<MenuItem>("MenuItems");
if (!menuItems.Any())
{
var defaultMenuItems = new[]
{
new MenuItem { Label = "Home", Url = "/", DisplayOrder = 1, IsActive = true },
new MenuItem { Label = "Shop", Url = "/Shop", DisplayOrder = 2, IsActive = true },
new MenuItem { Label = "Top Sellers", Url = "/#top-sellers", DisplayOrder = 3, IsActive = true },
new MenuItem { Label = "Promotion", Url = "/#promotion", DisplayOrder = 4, IsActive = true },
new MenuItem { Label = "Portfolio", Url = "/Portfolio", DisplayOrder = 5, IsActive = true },
new MenuItem { Label = "Blog", Url = "/Blog", DisplayOrder = 6, IsActive = true },
new MenuItem { Label = "About", Url = "/About", DisplayOrder = 7, IsActive = true },
new MenuItem { Label = "Instagram", Url = "#instagram", DisplayOrder = 8, IsActive = true },
new MenuItem { Label = "Contact", Url = "/Contact", DisplayOrder = 9, IsActive = true },
new MenuItem { Label = "My Wishlist", Url = "#wishlist", DisplayOrder = 10, IsActive = true }
};
foreach (var item in defaultMenuItems)
{
await mongoService.InsertAsync("MenuItems", item);
}
}
// Initialize homepage sections
var sections = await mongoService.GetAllAsync<HomepageSection>("HomepageSections");
if (!sections.Any())
{
var defaultSections = new[]
{
new HomepageSection
{
SectionType = "hero",
Title = "Scrapbooking and Journaling Fun",
Subtitle = "Explore the world of creativity and self-expression.",
ButtonText = "Shop Now",
ButtonUrl = "/Shop",
ImageUrl = "/assets/images/hero-craft.jpg",
DisplayOrder = 0,
IsActive = true
},
new HomepageSection
{
SectionType = "inspiration",
Title = "Our Inspiration",
Content = @"<p>Sky Art Shop specializes in scrapbooking, journaling, cardmaking, and collaging stationery. We aim to promote mental health through creative art activities.</p><p>Our offerings include washi tape, stickers, journals, and more.</p>",
ImageUrl = "/assets/images/craft-supplies.jpg",
DisplayOrder = 1,
IsActive = true
},
new HomepageSection
{
SectionType = "promotion",
Title = "Special Offers",
Content = @"<div class='promo-card featured'><h2>Large Stationery Mystery Bags</h2><p>Enjoy $70 worth of items for only $25. Think Larger and heavier items.</p><a href='tel:+5016080409' class='btn btn-primary'>Message Us</a></div>",
DisplayOrder = 2,
IsActive = true
}
};
foreach (var section in defaultSections)
{
await mongoService.InsertAsync("HomepageSections", section);
}
}
}

View File

@@ -1,211 +0,0 @@
# Quick IIS Deployment Guide - Sky Art Shop
## ⚡ Simplified Deployment (5 Steps)
### Step 1: Install Prerequisites (One-Time)
**A. Enable IIS**
- Run PowerShell as Administrator
- Execute:
```powershell
cd "E:\Documents\Website Projects\Sky_Art_Shop"
.\deploy.ps1 -InstallIIS
```
- **Restart your computer**
**B. Install .NET 8.0 Hosting Bundle**
- Download from: <https://dotnet.microsoft.com/download/dotnet/8.0>
- Look for "Hosting Bundle"
- Install and **restart computer**
### Step 2: Deploy Your Site
Run PowerShell as **Administrator**:
```powershell
cd "E:\Documents\Website Projects\Sky_Art_Shop"
.\deploy.ps1 -CreateSite
```
That's it! The script handles everything:
- ✅ Publishing the application
- ✅ Creating IIS site
- ✅ Setting permissions
- ✅ Configuring firewall
### Step 3: Verify MongoDB is Running
```powershell
# Check if MongoDB is running
Get-Service -Name MongoDB* -ErrorAction SilentlyContinue
# If not running, start it
net start MongoDB
```
### Step 4: Test Locally
1. Open browser
2. Go to: <http://localhost>
3. You should see your site!
**If you get errors:**
```powershell
# Check IIS status
Get-WebSite -Name "SkyArtShop"
# Restart IIS
iisreset /restart
# Check what's using port 80
netstat -ano | findstr :80
```
### Step 5: Configure Network (For Internet Access)
**A. Set Static IP**
1. Control Panel → Network → Properties
2. Set static IP (e.g., 192.168.1.100)
**B. Router Port Forwarding**
1. Login to your router (usually 192.168.1.1)
2. Forward port 80 → your PC's IP (192.168.1.100)
**C. No-IP Client**
1. Download: <https://www.noip.com/download>
2. Install and login
3. Your site will be at: <http://your-hostname.ddns.net>
---
## 🔄 Updating Your Site
When you make changes:
```powershell
cd "E:\Documents\Website Projects\Sky_Art_Shop"
.\deploy.ps1 -UpdateOnly
```
---
## 🆘 Quick Troubleshooting
**Site won't load locally:**
```powershell
# Restart IIS
iisreset /restart
# Check site status
Get-WebSite -Name "SkyArtShop"
# If stopped, start it
Start-WebSite -Name "SkyArtShop"
```
**502.5 Error:**
- You need to install .NET 8.0 Hosting Bundle
- Restart computer after installing
**403 Forbidden:**
```powershell
# Fix permissions
icacls "C:\inetpub\wwwroot\skyartshop" /grant "IIS_IUSRS:(OI)(CI)F" /T
iisreset /restart
```
**Can't access from internet:**
- Check port forwarding on router
- Make sure No-IP DUC is running
- Test your public IP directly first
---
## 📝 Your URLs After Deployment
- **Local**: <http://localhost>
- **Local Network**: <http://192.168.1.100> (your IP)
- **Internet**: <http://your-hostname.ddns.net>
---
## ⚙️ Manual Deployment (If Script Fails)
If the automated script doesn't work, follow these steps:
### 1. Publish Application
```powershell
cd "E:\Documents\Website Projects\Sky_Art_Shop"
dotnet publish SkyArtShop.csproj -c Release -o "C:\inetpub\wwwroot\skyartshop"
```
### 2. Create IIS Site Manually
1. Open IIS Manager (search in Windows Start)
2. Right-click Sites → Add Website
3. Site name: **SkyArtShop**
4. Physical path: **C:\inetpub\wwwroot\skyartshop**
5. Port: **80**
6. Click OK
### 3. Configure Application Pool
1. Click Application Pools
2. Right-click SkyArtShop → Basic Settings
3. .NET CLR version: **No Managed Code**
4. OK
### 4. Set Permissions
```powershell
icacls "C:\inetpub\wwwroot\skyartshop" /grant "IIS_IUSRS:(OI)(CI)F" /T
icacls "C:\inetpub\wwwroot\skyartshop" /grant "IUSR:(OI)(CI)F" /T
```
### 5. Configure Firewall
```powershell
New-NetFirewallRule -DisplayName "SkyArtShop-HTTP" -Direction Inbound -Protocol TCP -LocalPort 80 -Action Allow
```
### 6. Start Site
```powershell
Start-WebSite -Name "SkyArtShop"
```
---
## 🎯 Deployment Checklist
- [ ] IIS installed
- [ ] .NET 8.0 Hosting Bundle installed
- [ ] Computer restarted after installations
- [ ] MongoDB running
- [ ] Application published to C:\inetpub\wwwroot\skyartshop
- [ ] IIS site created
- [ ] Application pool set to "No Managed Code"
- [ ] Permissions granted
- [ ] Firewall rule added
- [ ] Site accessible at <http://localhost>
- [ ] (Optional) Static IP configured
- [ ] (Optional) Router port forwarding
- [ ] (Optional) No-IP client installed
---
**For the full detailed guide, see DEPLOYMENT_GUIDE.md**

View File

@@ -1,212 +0,0 @@
# 🎯 Sky Art Shop - Quick Reference Card
## 🚀 Start Backend
```powershell
cd "e:\Documents\Website Projects\Sky_Art_Shop\Admin"
dotnet run --launch-profile https
```
**URL**: <https://localhost:5001>
---
## 🔑 Admin Access
**URL**: <https://localhost:5001/admin>
**User**: `admin`
**Pass**: `admin123`
---
## 📁 File Structure
```
Sky_Art_Shop/
├── Admin/ # Backend CMS
│ ├── Controllers/
│ │ ├── AdminController.cs # Admin CRUD
│ │ ├── PublicProductsController.cs
│ │ ├── PublicProjectsController.cs
│ │ ├── PublicBlogController.cs
│ │ └── ... (more public APIs)
│ ├── Models/
│ ├── Views/Admin/ # Admin pages
│ ├── wwwroot/uploads/ # Uploaded images
│ └── Program.cs # CORS + routes
├── js/ # Frontend integration
│ ├── api-integration.js # Core API functions
│ ├── shop-page.js
│ ├── portfolio-page.js
│ └── blog-page.js
├── css/
│ └── api-styles.css # Card & grid styles
├── shop.html # Static pages
├── portfolio.html
├── blog.html
├── index.html
├── about.html
├── contact.html
├── test-api.html # Test page
├── INTEGRATION_GUIDE.md # How to integrate
├── IMAGE_FIX_GUIDE.md # Fix images
└── CMS_COMPLETE_GUIDE.md # Full overview
```
---
## 🔗 API Endpoints
| URL | Returns |
|-----|---------|
| `/api/products` | All products |
| `/api/projects` | Portfolio projects |
| `/api/blog` | Blog posts |
| `/api/pages/{slug}` | Page content |
| `/api/categories` | Categories |
| `/api/settings` | Site settings |
| `/uploads/products/*` | Product images |
---
## 📝 Integration Template
**For any HTML page:**
```html
<!-- Before </body> -->
<link rel="stylesheet" href="css/api-styles.css">
<script src="js/api-integration.js"></script>
<script src="js/shop-page.js"></script> <!-- or portfolio-page.js, etc. -->
```
**In HTML body:**
```html
<div id="productsContainer"></div> <!-- For products -->
<div id="projectsContainer"></div> <!-- For projects -->
<div id="blogContainer"></div> <!-- For blog -->
```
---
## 🧪 Testing
1. **Test API**: Open `test-api.html`
2. **Test Page**: Open `shop.html` (after integration)
3. **Check Console**: F12 → Console → Should see "Loaded X items"
4. **Check Network**: F12 → Network → See API calls + images
---
## 🐛 Quick Fixes
### Images Not Showing
1. Admin → Products → Edit
2. Upload Main Image
3. Save
4. Refresh static page
### Backend Not Running
```powershell
cd "e:\Documents\Website Projects\Sky_Art_Shop\Admin"
dotnet run --launch-profile https
```
### CORS Error
Already fixed - CORS enabled in Program.cs
### Can't See Products
1. Check browser console (F12)
2. Verify backend running
3. Test with `test-api.html`
---
## 📂 Image Upload Locations
| Content Type | Upload Folder |
|-------------|---------------|
| Products | `Admin/wwwroot/uploads/products/` |
| Projects | `Admin/wwwroot/uploads/projects/` |
| Blog | `Admin/wwwroot/uploads/blog/` |
**Served at**: `https://localhost:5001/uploads/products/<file>`
---
## ✅ Integration Checklist
- [ ] Backend running (<https://localhost:5001>)
- [ ] MongoDB service running
- [ ] Products have images uploaded
- [ ] Script tags added to HTML pages
- [ ] Container divs added to HTML
- [ ] `test-api.html` shows products with images
- [ ] `shop.html` renders products from API
- [ ] Browser console shows "Loaded X products"
---
## 📚 Documentation
| File | Purpose |
|------|---------|
| `INTEGRATION_GUIDE.md` | Step-by-step for each page |
| `IMAGE_FIX_GUIDE.md` | Fix missing images |
| `CMS_COMPLETE_GUIDE.md` | Full overview |
| `test-api.html` | Test & debug page |
---
## 🎯 Workflow
**For Client (Content Management):**
1. Open <https://localhost:5001/admin>
2. Login
3. Add/Edit products, projects, blog posts
4. Upload images
5. Save
6. Changes appear on static site immediately
**For Developer (Customization):**
1. Edit `js/api-integration.js` for custom rendering
2. Edit `css/api-styles.css` for styling
3. Edit HTML pages for layout
4. Backend APIs remain unchanged
---
## 💡 Pro Tips
- **Test First**: Always use `test-api.html` to verify API before integrating
- **Console is Your Friend**: F12 → Console shows all errors
- **Images**: Upload via Admin Edit, not Create (safer)
- **CORS**: Already configured for `file://` and `localhost`
- **Production**: Change `API_BASE` in `api-integration.js` when deploying
---
## 🚀 What's Next?
1. **Upload images** to products (Admin → Products → Edit)
2. **Integrate HTML pages** (add script tags + container divs)
3. **Test everything** with `test-api.html`
4. **Customize styles** in `api-styles.css`
5. **Deploy** (optional - see CMS_COMPLETE_GUIDE.md)
---
**Status**: ✅ Backend Running | ⚠️ Images Need Upload | 📝 Pages Need Integration
**Quick Test**: Open `file:///E:/Documents/Website%20Projects/Sky_Art_Shop/test-api.html`

View File

@@ -1,314 +0,0 @@
# Sky Art Shop - ASP.NET Core CMS
A dynamic e-commerce CMS for Sky Art Shop built with ASP.NET Core MVC, MongoDB, and ASP.NET Core Identity. Specializing in scrapbooking, journaling, cardmaking, and collaging stationery.
## 📋 Project Overview
Sky Art Shop promotes mental health through creative art activities. This CMS enables the shop owner to manage products, portfolio, blog posts, and pages through a secure admin panel without touching code.
## 🎨 Features
### Content Management
- **Products**: Full CRUD with categories, pricing, images, and inventory tracking
- **Portfolio**: Categories and projects with image galleries
- **Blog**: Posts with rich text editing, featured images, tags, and publishing controls
- **Pages**: Custom pages with dynamic content (About, Contact, etc.)
- **Site Settings**: Global configuration (site name, contact info, hero section, footer)
- **Navigation**: Dynamic menu management stored in MongoDB
### Admin Panel
- Secure authentication with ASP.NET Core Identity
- Role-based access control (Admin role)
- Bootstrap 5 dashboard interface
- CKEditor 5 rich text editor (no API key required)
- Image upload management
- Real-time feedback with TempData alerts
### Technology Stack
- **Backend**: ASP.NET Core 8.0 MVC
- **Content Database**: MongoDB (products, pages, portfolio, blog, settings, menus)
- **Authentication Database**: SQLite + Entity Framework Core + ASP.NET Core Identity
- **Frontend**: Razor Views, Bootstrap 5, custom CSS
- **Rich Text Editor**: CKEditor 5 (free, no API key)
- **Image Storage**: File system (wwwroot/uploads/images)
## 📁 Project Structure
```
Sky_Art_Shop/
├── Controllers/ # MVC controllers (public + admin)
├── Data/ # EF Core DbContext for Identity
├── Models/ # MongoDB data models
├── Services/ # MongoDBService
├── ViewComponents/ # Navigation ViewComponent
├── Views/
│ ├── Home/ # Public homepage
│ ├── Shop/ # Products listing
│ ├── Portfolio/ # Portfolio pages
│ ├── Blog/ # Blog pages
│ ├── About/ # About page
│ ├── Contact/ # Contact page
│ ├── Admin/ # Admin dashboard
│ ├── AdminProducts/ # Product management
│ ├── AdminPortfolio/ # Portfolio management
│ ├── AdminBlog/ # Blog management
│ ├── AdminPages/ # Pages management
│ ├── AdminSettings/ # Settings management
│ └── Shared/ # Layouts and partials
├── wwwroot/
│ ├── assets/ # Static files (CSS, JS, images)
│ └── uploads/ # User-uploaded images
├── appsettings.json # Configuration
├── Program.cs # Application entry point
└── SkyArtShop.csproj # Project file
```
### Prerequisites
- .NET 8.0 SDK
- MongoDB server (local or Atlas)
- Git (optional)
### Configuration
1. **MongoDB Connection**
Update `appsettings.json` with your MongoDB connection string:
```json
{
"MongoDB": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "SkyArtShopDB"
}
}
```
2. **Admin User Credentials**
Default admin login (configure in `appsettings.json`):
```json
{
"AdminUser": {
"Email": "admin@skyartshop.com",
"Password": "Admin123!",
"Name": "Admin"
}
}
```
### Installation
1. **Restore dependencies**
```powershell
dotnet restore
```
2. **Build the project**
```powershell
dotnet build
```
3. **Run the application**
```powershell
dotnet run
```
4. **Access the site**
- Public site: <http://localhost:5000>
- Admin panel: <http://localhost:5000/admin/login>
### First Run
On first run, the application automatically:
- Creates SQLite database for Identity (identity.db)
- Seeds Admin role and user
- Creates default site settings
- Creates default portfolio categories
- Creates About page
- Creates navigation menu items
## 📱 Admin Panel Usage
### Login
Navigate to <http://localhost:5000/admin/login> and use the credentials from appsettings.json.
### Dashboard
View counts for products, projects, blog posts, and pages.
### Managing Products
1. Go to **Admin → Products**
2. Click **Create Product**
3. Fill in product details (name, description, price, category, stock)
4. Upload product image
5. Mark as Featured or Top Seller if desired
6. Click **Save**
### Managing Portfolio
1. **Categories**: Create categories first (Displays, Card Making, etc.)
2. **Projects**: Add projects under each category with images and descriptions
### Managing Blog
1. Go to **Admin → Blog**
2. Click **Create Post**
3. Use CKEditor for rich content formatting
4. Add featured image and excerpt
5. Add tags (comma-separated)
6. Toggle **Published** when ready to make live
### Managing Pages
1. Go to **Admin → Pages**
2. Edit existing pages (About) or create new ones
3. Use CKEditor for content formatting
4. Enable/disable with **Active** checkbox
### Site Settings
Update global settings like site name, contact info, hero section, and footer text.
### Image Uploads
- Click **Upload** button in any form with image fields
- Supports JPG, PNG, GIF, WEBP
- Images saved to wwwroot/uploads/images
- Copy image URLs to use in CKEditor content
## 🌐 Public Pages
### Home (/)
Dynamic hero section, site settings, and top seller products from MongoDB.
### Shop (/shop)
All products with category filtering.
### Portfolio (/portfolio)
Browse portfolio categories and projects.
### Blog (/blog)
Published blog posts with individual post pages (/blog/post/{slug}).
### About (/about)
Dynamic content from Pages collection.
### Contact (/contact)
Contact form (TODO: email sending integration)
## 🗄️ Database Collections (MongoDB)
- **Products**: E-commerce items with pricing, categories, images
- **PortfolioCategories**: Portfolio organization
- **PortfolioProjects**: Portfolio items with images and descriptions
- **BlogPosts**: Blog articles with rich content
- **Pages**: Custom pages (About, etc.)
- **SiteSettings**: Global configuration
- **MenuItems**: Navigation menu structure
## 🔒 Security
- ASP.NET Core Identity for authentication
- Role-based authorization (Admin role required)
- HTTPS redirection enabled
- Session cookies with HttpOnly flag
- Password requirements (configurable)
- SQL injection protection via EF Core
- XSS protection via Razor encoding
## 🛠️ Troubleshooting
### MongoDB Connection Issues
- Verify MongoDB is running
- Check connection string in appsettings.json
- Ensure network access if using MongoDB Atlas
### Identity Database Issues
- Delete identity.db to recreate
- Check file permissions in project directory
### Image Upload Issues
- Ensure wwwroot/uploads/images directory exists
- Check write permissions
### Build Errors
```powershell
dotnet clean
dotnet build
```
## 📈 Future Enhancements (Optional)
- [ ] Server-side validation with DataAnnotations
- [ ] Centralized slug generation service
- [ ] Email service integration for contact form
- [ ] Shopping cart and checkout functionality
- [ ] Product search and advanced filtering
- [ ] Image gallery management for products/projects
- [ ] SEO meta tags management
- [ ] Multi-language support
- [ ] Analytics integration
- [ ] Automated backups
## 📞 Support
For questions or issues:
- Email: <info@skyartshop.com>
- Phone: +501 608-0409
- Email: <info@skyartshop.com>
- Instagram: @skyartshop
## 📝 License
© 2035 by Sky Art Shop. All rights reserved.
## 🎯 SEO Optimization
The site includes:
- Semantic HTML structure
- Meta descriptions on all pages
- Descriptive alt text for images (to be added)
- Clean URL structure
- Fast loading times
## ⚡ Performance Tips
1. Compress images before uploading (recommended: WebP format)
2. Use appropriate image sizes (max width 1920px for hero images)
3. Consider using a CDN for assets
4. Enable browser caching
5. Minify CSS and JavaScript for production
## 🔐 Security Notes
- Form submissions should be processed server-side
- Add HTTPS when deploying to production
- Implement CSRF protection for forms
- Sanitize all user inputs
---
**Built with ❤️ for Sky Art Shop**
*Promoting mental health through creative art activities*

View File

@@ -1,343 +0,0 @@
# 🚀 Sky Art Shop Website - Quick Start Guide
Welcome! This guide will help you get your Sky Art Shop website up and running quickly.
## ✅ What's Included
Your website includes:
- ✨ 8 fully functional HTML pages
- 🎨 Complete responsive CSS styling
- 💻 Interactive JavaScript features
- 📁 Organized file structure
- 📖 Comprehensive documentation
## 📋 Step-by-Step Setup
### Step 1: View Your Website
#### Option A: Using Live Server (Recommended)
1. Open Visual Studio Code
2. Install "Live Server" extension if not already installed:
- Click Extensions icon (or press `Ctrl+Shift+X`)
- Search for "Live Server"
- Click Install
3. Right-click on `index.html`
4. Select "Open with Live Server"
5. Your website will open in your default browser!
#### Option B: Open Directly in Browser
1. Navigate to your project folder
2. Double-click `index.html`
3. The website will open in your default browser
### Step 2: Add Your Images
**Important**: The website currently references image files that don't exist yet. You need to add them!
1. Read the `IMAGE-GUIDE.md` file for complete image requirements
2. Create the necessary folders:
```
assets/images/
assets/images/products/
assets/images/portfolio/
assets/images/portfolio/displays/
assets/images/portfolio/personal-crafts/
assets/images/portfolio/card-making/
assets/images/portfolio/scrapbook-albums/
assets/images/blog/
```
3. Add your images to these folders
4. Or use placeholder images temporarily (see IMAGE-GUIDE.md)
### Step 3: Customize Your Content
#### Update Business Information
1. Open `contact.html`
2. Update:
- Phone number
- Email address
- Business hours
- Social media links
#### Update About Page
1. Open `about.html`
2. Customize:
- Your story
- Mission statement
- Product offerings
#### Add Your Products
1. Open `shop.html`
2. Find the product cards
3. Update:
- Product names
- Prices
- Descriptions
- Images
### Step 4: Customize Colors & Branding
1. Open `assets/css/main.css`
2. Find the `:root` section at the top
3. Change the color variables:
```css
:root {
--primary-color: #6B4E9B; /* Your brand's main color */
--secondary-color: #E91E63; /* Accent color */
--accent-color: #FF9800; /* Highlight color */
}
```
4. Save and refresh your browser to see changes
## 🎯 Key Pages Explained
### 1. Home Page (`index.html`)
- Main landing page with hero section
- Features products and promotions
- Links to all other sections
### 2. Portfolio Page (`portfolio.html`)
- Showcases your creative work
- Links to 4 category pages
- Great for displaying past projects
### 3. Shop Page (`shop.html`)
- Displays all products
- Has filtering by category
- Sorting by price functionality
### 4. About Page (`about.html`)
- Tell your story
- Explain your mission
- Build trust with customers
### 5. Contact Page (`contact.html`)
- Contact form (currently client-side only)
- Business information
- Social media links
### 6. Blog Page (`blog.html`)
- Share tips and tutorials
- Build community
- Improve SEO
## 🎨 Customization Tips
### Changing Fonts
In `main.css`, update these variables:
```css
--font-primary: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
--font-heading: 'Georgia', serif;
```
### Adjusting Spacing
Modify these spacing variables:
```css
--spacing-sm: 1rem;
--spacing-md: 2rem;
--spacing-lg: 3rem;
--spacing-xl: 4rem;
```
### Adding New Products
Copy an existing product card in `shop.html`:
```html
<div class="product-card" data-category="your-category">
<div class="product-image">
<img src="assets/images/products/your-product.jpg" alt="Product Name">
</div>
<h3>Your Product Name</h3>
<p class="product-description">Product description</p>
<p class="price">$XX.XX</p>
<button class="btn btn-small">Add to Cart</button>
</div>
```
## 📱 Testing Your Website
### Test on Different Devices
1. **Desktop**: Full features and layout
2. **Tablet** (768px): Adjusted grid layouts
3. **Mobile** (480px): Mobile menu and stacked layout
### Test in Different Browsers
- Chrome
- Firefox
- Safari
- Edge
### Use Browser Developer Tools
1. Press `F12` in your browser
2. Click the device toolbar icon
3. Test different screen sizes
## 🔧 Common Issues & Solutions
### Issue: Images Don't Show
**Solution**:
- Check file paths are correct
- Ensure image files exist in the right folders
- Check file names match exactly (case-sensitive)
### Issue: Navigation Menu Doesn't Work on Mobile
**Solution**:
- Make sure `main.js` is properly linked
- Check browser console for JavaScript errors (F12)
- Clear browser cache and reload
### Issue: Styles Not Applying
**Solution**:
- Verify `main.css` is in `assets/css/` folder
- Check the CSS file is linked in your HTML
- Clear browser cache (Ctrl+Shift+R)
### Issue: Contact Form Doesn't Submit
**Note**: The contact form currently works client-side only (shows notification but doesn't actually send emails). To make it functional:
1. Set up a backend server (PHP, Node.js, etc.)
2. Or use a service like Formspree or Netlify Forms
3. Or integrate with an email API
## 🌐 Next Steps: Going Live
### Option 1: Traditional Web Hosting
1. Choose a hosting provider (Bluehost, HostGator, etc.)
2. Upload files via FTP
3. Point your domain to the hosting
### Option 2: GitHub Pages (Free)
1. Create a GitHub account
2. Create a new repository
3. Upload your files
4. Enable GitHub Pages in settings
### Option 3: Netlify (Free)
1. Create a Netlify account
2. Drag and drop your project folder
3. Get a free subdomain or connect your own
### Before Going Live
- [ ] Add all your images
- [ ] Update all content
- [ ] Test all links
- [ ] Add meta descriptions for SEO
- [ ] Set up a contact form backend
- [ ] Add Google Analytics (optional)
- [ ] Get an SSL certificate (HTTPS)
## 📊 Adding Analytics
To track visitors, add Google Analytics:
1. Create a Google Analytics account
2. Get your tracking code
3. Add it before the closing `</head>` tag in all HTML files:
```html
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=YOUR-ID"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'YOUR-ID');
</script>
```
## 🛒 Adding E-commerce Features
The current site has basic "Add to Cart" buttons. To make them functional:
### Option 1: Shopify Buy Button
- Easy to integrate
- Handles payments securely
- No coding required
### Option 2: WooCommerce
- Works with WordPress
- Full e-commerce features
- Requires more setup
### Option 3: Custom Solution
- Build shopping cart in JavaScript
- Set up payment gateway (Stripe, PayPal)
- Requires programming knowledge
## 📞 Need Help?
### Resources
- **HTML/CSS Help**: [MDN Web Docs](https://developer.mozilla.org/)
- **JavaScript Help**: [JavaScript.info](https://javascript.info/)
- **Web Hosting**: [Netlify](https://www.netlify.com/), [GitHub Pages](https://pages.github.com/)
### Documentation Files
- `README.md` - Complete project overview
- `IMAGE-GUIDE.md` - Image requirements and tips
- `SETUP-GUIDE.md` - This file!
## ✨ Tips for Success
1. **Start Simple**: Don't try to add everything at once
2. **Test Often**: Check your changes in the browser frequently
3. **Use Version Control**: Consider using Git to track changes
4. **Backup Regularly**: Keep copies of your work
5. **Get Feedback**: Show friends/family and get their input
6. **Stay Organized**: Keep your files and folders well-organized
## 🎉 You're Ready
Your Sky Art Shop website is ready to customize and launch. Take it step by step, and don't hesitate to experiment!
Remember:
- The website is fully responsive and mobile-friendly
- All code is well-commented and organized
- You can customize colors, fonts, and content easily
- Images need to be added before going live
**Good luck with your Sky Art Shop! 🎨✨**
---
*Need to make changes? Just edit the HTML, CSS, or JavaScript files and refresh your browser!*

View File

@@ -1,352 +0,0 @@
# Sky Art Shop - Website Structure & Sitemap
## 📂 Complete File Structure
```
Sky_Art_Shop/
├── 📄 index.html # Home page (Main landing page)
├── 📄 portfolio.html # Portfolio gallery page
├── 📄 shop.html # Product shop page
├── 📄 about.html # About us page
├── 📄 contact.html # Contact page
├── 📄 blog.html # Blog page
├── 📄 README.md # Project documentation
├── 📄 SETUP-GUIDE.md # Quick start guide
├── 📄 IMAGE-GUIDE.md # Image requirements
├── 📄 SITEMAP.md # This file
├── 📁 portfolio/ # Portfolio category pages
│ ├── 📄 displays.html # Displays category
│ ├── 📄 personal-craft-projects.html # Personal crafts category
│ ├── 📄 card-making.html # Card making category
│ └── 📄 scrapbook-albums.html # Scrapbook albums category
├── 📁 assets/ # All website assets
│ ├── 📁 css/
│ │ └── 📄 main.css # Main stylesheet (15KB)
│ │
│ ├── 📁 js/
│ │ └── 📄 main.js # Main JavaScript file
│ │
│ └── 📁 images/ # Image assets folder
│ ├── hero-craft.jpg # (Add your images here)
│ ├── craft-supplies.jpg
│ ├── 📁 products/ # Product images
│ ├── 📁 portfolio/ # Portfolio images
│ │ ├── 📁 displays/
│ │ ├── 📁 personal-crafts/
│ │ ├── 📁 card-making/
│ │ └── 📁 scrapbook-albums/
│ └── 📁 blog/ # Blog post images
└── 📁 includes/ # (Reserved for future partials)
```
## 🗺️ Visual Site Map
```
HOME (index.html)
|
┌─────────────┬───────────┼───────────┬──────────────┬──────────┐
| | | | | |
SHOP PORTFOLIO ABOUT CONTACT BLOG (More)
(shop.html) (portfolio.html) (about) (contact.html) (blog.html)
| |
| |
| ┌────┴────┬──────────────┬────────────────┐
| | | | |
| DISPLAYS PERSONAL CARD MAKING SCRAPBOOK
| (displays) CRAFTS (card-making) ALBUMS
| (personal-) (scrapbook-)
|
[Products]
```
## 📑 Page Inventory
### Main Pages (6)
1. **Home** (`index.html`) - Landing page
2. **Portfolio** (`portfolio.html`) - Work showcase
3. **Shop** (`shop.html`) - Products catalog
4. **About** (`about.html`) - Company info
5. **Contact** (`contact.html`) - Contact form
6. **Blog** (`blog.html`) - Blog posts
### Portfolio Category Pages (4)
7. **Displays** (`portfolio/displays.html`)
8. **Personal Craft Projects** (`portfolio/personal-craft-projects.html`)
9. **Card Making** (`portfolio/card-making.html`)
10. **Scrapbook Albums** (`portfolio/scrapbook-albums.html`)
**Total Pages: 10**
## 🧭 Navigation Structure
### Main Navigation Menu
```
┌────────────────────────────────────────────────────────┐
│ Sky Art Shop │
│ │
│ Home | Shop | Top Sellers | Promotion | Portfolio | │
│ Blog | About | Instagram | Contact | My Wishlist │
└────────────────────────────────────────────────────────┘
```
### Mobile Navigation
- Hamburger menu (☰)
- Slides in from left
- Same menu items
- Touch-friendly
## 🔗 Internal Links Map
### From Home Page
- → Shop (via buttons and nav)
- → Portfolio (via nav)
- → About (via nav)
- → Contact (via nav)
- → Blog (via nav)
- → Top Sellers (anchor link)
- → Promotion (anchor link)
### From Portfolio Page
- → Displays category
- → Personal Craft Projects category
- → Card Making category
- → Scrapbook Albums category
### From Category Pages
- ← Back to Portfolio (breadcrumb)
- → Individual project pages (future enhancement)
## 📊 Content Sections by Page
### Home Page Sections
1. Navigation Bar
2. Hero Section
3. Our Inspiration
4. Explore Collection
5. Promotion
6. Top Sellers
7. Footer
### Portfolio Page Sections
1. Navigation Bar
2. Hero Section
3. Portfolio Gallery (4 categories)
4. Footer
### Shop Page Sections
1. Navigation Bar
2. Hero Section
3. Filter Bar
4. Products Grid (12 products)
5. Footer
### About Page Sections
1. Navigation Bar
2. Hero Section
3. Our Story
4. What We Offer
5. Our Values
6. Footer
### Contact Page Sections
1. Navigation Bar
2. Hero Section
3. Contact Information
4. Contact Form
5. Footer
### Blog Page Sections
1. Navigation Bar
2. Hero Section
3. Blog Posts Grid (6 posts)
4. Footer
## 🎯 Call-to-Action (CTA) Buttons
### Primary CTAs
- "Shop Now" (Home hero → Shop page)
- "Add to Cart" (Shop page)
- "Send Message" (Contact form)
- "Read More" (Blog posts)
### Secondary CTAs
- "Message Us" (Promotion section)
- Portfolio category links
- Navigation menu items
## 📱 Responsive Breakpoints
```
Desktop: 1200px+ (Full layout)
Tablet: 768px (Adjusted grids)
Mobile: 480px (Stacked layout, hamburger menu)
```
## 🔍 SEO-Optimized URLs
### Recommended URL Structure (when hosted)
```
yourdomain.com/
yourdomain.com/shop
yourdomain.com/portfolio
yourdomain.com/portfolio/displays
yourdomain.com/portfolio/personal-craft-projects
yourdomain.com/portfolio/card-making
yourdomain.com/portfolio/scrapbook-albums
yourdomain.com/about
yourdomain.com/contact
yourdomain.com/blog
```
## 🎨 Page Templates
### Template A: Content Page
Used by: About
- Hero section
- Content grid
- Images
### Template B: Gallery Page
Used by: Portfolio, Portfolio Categories
- Hero section
- Grid layout
- Image cards with overlays
### Template C: E-commerce Page
Used by: Shop
- Hero section
- Filter bar
- Product grid
### Template D: Form Page
Used by: Contact
- Hero section
- Two-column layout
- Form + information
## 🔄 User Flows
### Flow 1: Browse & Shop
```
Home → Shop → [Filter/Sort] → Add to Cart → (Future: Checkout)
```
### Flow 2: Explore Portfolio
```
Home → Portfolio → Category Page → (Future: Project Details)
```
### Flow 3: Contact
```
Any Page → Contact → Fill Form → Submit
```
### Flow 4: Learn More
```
Home → About → (Optional: Blog) → Contact
```
## 📈 Future Expansion Areas
### Potential New Pages
1. Product detail pages
2. Individual project pages
3. Shopping cart page
4. Checkout page
5. User account pages
6. Blog post detail pages
7. Privacy policy page
8. Terms of service page
9. FAQ page
10. Testimonials page
### Potential New Features
1. Search functionality
2. Product reviews
3. Wishlist functionality
4. Newsletter signup
5. Live chat
6. Social media feed integration
7. Image lightbox/gallery viewer
8. Video content
9. Customer login area
10. Order tracking
## 🎯 Key Pages by User Intent
### Discovery
- Home
- Portfolio
- Blog
### Shopping
- Shop
- (Future) Product Details
- (Future) Cart
### Information
- About
- Contact
- Blog
### Action
- Contact Form
- Add to Cart buttons
- (Future) Checkout
## 📝 Notes
- All pages use consistent header and footer
- Navigation is identical across all pages
- Responsive design works on all pages
- Each page has appropriate meta descriptions (to be added)
- All internal links are relative paths
- Image paths use consistent structure
- CSS and JS files are shared across all pages
---
**Last Updated**: December 1, 2025
**Total Pages**: 10 HTML pages
**Total Folders**: 8 folders
**Total CSS Files**: 1 (main.css)
**Total JS Files**: 1 (main.js)

View File

@@ -1,291 +0,0 @@
# ✅ SKY ART SHOP - COMPLETE & WORKING
## 🎉 Status: FULLY OPERATIONAL
### Backend Status
-**Running**: <http://localhost:5000> & <https://localhost:5001>
-**API Working**: `/api/products` returns 2 products
-**Images Serving**: Images accessible at `/uploads/products/`
-**MongoDB Connected**: SkyArtShopCMS database
### Products in Database
1. **"anime book"** - ✅ HAS 4 IMAGES
- Price: $30.00
- Category: anime
- Images: 4 uploaded ✓
2. **"Sample Product"** - ⚠️ NO IMAGES YET
- Price: $25.00
- Category: General
- **Action needed**: Upload image via admin
---
## 🚀 WORKING DEMO
### Open This File Now
```
file:///E:/Documents/Website%20Projects/Sky_Art_Shop/shop-demo.html
```
This is a **complete, working shop page** that:
- ✅ Connects to your backend API
- ✅ Displays all products with images
- ✅ Shows prices and descriptions
- ✅ Has beautiful styling
- ✅ Shows error messages if backend is offline
**The demo page should already be open in your browser!**
---
## 📝 How to Integrate Into Your Actual shop.html
### Method 1: Use the Demo (Easiest)
Rename `shop-demo.html` to `shop.html` and you're done!
```powershell
cd "e:\Documents\Website Projects\Sky_Art_Shop"
Move-Item shop.html shop-old.html -Force
Move-Item shop-demo.html shop.html -Force
```
### Method 2: Add Code to Existing shop.html
See the file: `SHOP_HTML_INTEGRATION.html` for exact code to copy/paste.
**Quick version - add before `</body>`:**
```html
<div id="productsContainer" class="products-grid"></div>
<script>
const API_BASE = 'http://localhost:5000';
async function loadProducts() {
const container = document.getElementById('productsContainer');
try {
const response = await fetch(API_BASE + '/api/products');
const products = await response.json();
container.innerHTML = products.map(p => {
const img = p.mainImageUrl || (p.images && p.images[0]) || '';
const imgSrc = img ? API_BASE + img : '';
return `
<div class="product-card">
${imgSrc ? `<img src="${imgSrc}" alt="${p.name}">` : '<div class="no-image">No image</div>'}
<h3>${p.name}</h3>
<p class="price">$${p.price.toFixed(2)}</p>
</div>
`;
}).join('');
} catch (error) {
container.innerHTML = '<p class="error">Failed to load products: ' + error.message + '</p>';
}
}
document.addEventListener('DOMContentLoaded', loadProducts);
</script>
```
---
## 🖼️ Fix the "Sample Product" Missing Image
1. Open admin: <https://localhost:5001/admin/products>
2. Click **Edit** on "Sample Product"
3. Upload a **Main Image**
4. Click **Save**
5. Refresh shop page - image will appear!
---
## 🧪 Test Everything
### 1. Backend Test
```powershell
Invoke-RestMethod -Uri "http://localhost:5000/api/products" | Format-List name, price, images
```
**Expected**: See 2 products, "anime book" has 4 images
### 2. Image Test
```powershell
Invoke-WebRequest -Uri "http://localhost:5000/uploads/products/2dbdad6c-c4a6-4f60-a1ce-3ff3b88a13ae.jpg" -Method Head
```
**Expected**: Status 200 OK
### 3. Shop Page Test
Open: `file:///E:/Documents/Website%20Projects/Sky_Art_Shop/shop-demo.html`
**Expected**: See 2 products, "anime book" shows image
---
## 📁 Files Created for You
| File | Purpose |
|------|---------|
| `shop-demo.html` | **Complete working shop page** (use this!) |
| `SHOP_HTML_INTEGRATION.html` | Code snippets to add to existing shop.html |
| `test-api.html` | Test page for debugging API issues |
| `js/api-integration.js` | Reusable API functions |
| `js/shop-page.js` | Shop-specific integration |
| `css/api-styles.css` | Professional product card styling |
| `INTEGRATION_GUIDE.md` | Detailed integration instructions |
| `IMAGE_FIX_GUIDE.md` | How to upload images |
| `CMS_COMPLETE_GUIDE.md` | Full system documentation |
| `QUICK_REFERENCE.md` | Quick commands & tips |
---
## 🎯 Current Status
| Item | Status | Notes |
|------|--------|-------|
| Backend Running | ✅ YES | <http://localhost:5000> |
| API Working | ✅ YES | Returns 2 products |
| Images Serving | ✅ YES | HTTP serving works |
| Demo Page | ✅ WORKING | shop-demo.html |
| "anime book" Images | ✅ YES | 4 images uploaded |
| "Sample Product" Images | ⚠️ NO | Need to upload |
| shop.html Integration | ⏳ PENDING | Use shop-demo.html or add code |
---
## ✅ What Works Right Now
1. **Backend CMS** - Admin can add/edit products ✓
2. **Image Upload** - Upload via admin works ✓
3. **Image Serving** - Images accessible via HTTP ✓
4. **API Endpoints** - All 6 APIs working ✓
5. **Demo Shop Page** - Fully functional ✓
6. **Product Display** - "anime book" shows with image ✓
---
## 🔧 Next Steps (Optional)
### Immediate (5 minutes)
1.**DONE**: Demo page is working
2. Upload image to "Sample Product" (optional)
3. Replace your shop.html with shop-demo.html (or keep both)
### Soon
1. Integrate portfolio.html (same pattern as shop)
2. Integrate blog.html
3. Customize styles in css/api-styles.css
4. Add more products in admin
### Later
1. Deploy backend to Azure/AWS
2. Use MongoDB Atlas (cloud database)
3. Update API_BASE to production URL
4. Add shopping cart functionality
---
## 🐛 Troubleshooting
### Products Don't Show
**Check**: Is backend running?
```powershell
Get-Process | Where-Object {$_.ProcessName -like "*SkyArtShop*"}
```
**Fix**: Start backend
```powershell
cd "e:\Documents\Website Projects\Sky_Art_Shop\Admin"
dotnet run --launch-profile https
```
### Images Don't Load
**Check**: Do products have images in database?
```powershell
Invoke-RestMethod -Uri "http://localhost:5000/api/products" | Select-Object name, images
```
**Fix**: Upload images via admin
- Open: <https://localhost:5001/admin/products>
- Edit product → Upload Main Image → Save
### CORS Errors
**Already Fixed!** CORS is enabled in Program.cs
### Page is Blank
**Check**: Open DevTools (F12) → Console for errors
**Fix**: Verify container div exists:
```html
<div id="productsContainer"></div>
```
---
## 📞 Quick Reference Commands
```powershell
# Start backend
cd "e:\Documents\Website Projects\Sky_Art_Shop\Admin"
dotnet run --launch-profile https
# Test API
Invoke-RestMethod -Uri "http://localhost:5000/api/products"
# Open demo
Start-Process "file:///E:/Documents/Website%20Projects/Sky_Art_Shop/shop-demo.html"
# Open admin
Start-Process "https://localhost:5001/admin"
# Check backend process
Get-Process | Where-Object {$_.ProcessName -like "*SkyArtShop*"}
```
---
## 🎊 SUCCESS
Your Sky Art Shop is **fully operational**:
- ✅ Backend CMS running
- ✅ Public API working
- ✅ Images loading
- ✅ Demo shop page displaying products
- ✅ Admin panel accessible
- ✅ MongoDB connected
**Open the demo now**: `file:///E:/Documents/Website%20Projects/Sky_Art_Shop/shop-demo.html`
You should see:
- "anime book" with image ($30.00)
- "Sample Product" without image ($25.00)
**That's it! Your CMS-powered shop is live!** 🎉
---
*Last updated: December 1, 2025*
*Backend: ✅ Running | API: ✅ Working | Images: ✅ Serving*

View File

@@ -1,257 +0,0 @@
# Sky Art Shop - System Status Report
**Generated:** December 3, 2025
**Status:** ✅ OPERATIONAL
---
## 🎯 System Overview
**Framework:** ASP.NET Core MVC 8.0
**Content Database:** MongoDB (localhost:27017)
**Auth Database:** SQLite (Identity)
**Running Port:** <http://localhost:5001>
**Environment:** Production
---
## 📊 MongoDB Collections (All Connected & Working)
### Content Collections
| Collection | Purpose | Status | Controller |
|------------|---------|--------|------------|
| **Pages** | Dynamic pages (About, etc.) | ✅ Active | AboutController, AdminPagesController, PageController |
| **Products** | Shop products | ✅ Active | ShopController, AdminProductsController, HomeController |
| **BlogPosts** | Blog articles | ✅ Active | BlogController, AdminBlogController |
| **PortfolioCategories** | Portfolio categories | ✅ Active | PortfolioController, AdminPortfolioController |
| **PortfolioProjects** | Portfolio projects | ✅ Active | PortfolioController, AdminPortfolioController |
| **HomepageSections** | Dynamic homepage sections | ✅ Active | HomeController, AdminHomepageController |
| **SiteSettings** | Global site settings | ✅ Active | AdminSettingsController, HomeController, ContactController |
| **MenuItems** | Navigation menu items | ✅ Active | NavigationViewComponent, AdminMenuController |
### Seeded Data Status
- ✅ Default Admin User: <admin@skyartshop.com> / Admin123!
- ✅ Admin Role configured
- ✅ Default Site Settings created
- ✅ Default Portfolio Categories (4) created
- ✅ About Page initialized
- ✅ Default Menu Items (10) created
- ✅ Homepage Sections (3) created
---
## 🎨 Frontend Pages (All Accessible)
### Public Pages
| Page | Route | Data Source | Status |
|------|-------|-------------|--------|
| Home | `/` | HomepageSections, Products, SiteSettings | ✅ Working |
| Shop | `/shop` | Products | ✅ Working |
| Product Detail | `/shop/{slug}` | Products | ✅ Working |
| Portfolio | `/portfolio` | PortfolioCategories, PortfolioProjects | ✅ Working |
| Portfolio Category | `/portfolio/category/{slug}` | PortfolioProjects | ✅ Working |
| Blog | `/blog` | BlogPosts | ✅ Working |
| Blog Post | `/blog/{slug}` | BlogPosts | ✅ Working |
| About | `/about` | Pages (with ImageGallery & TeamMembers) | ✅ Working |
| Contact | `/contact` | SiteSettings | ✅ Working |
| Dynamic Pages | `/page/{slug}` | Pages | ✅ Working |
### Admin Pages
| Page | Route | Purpose | Status |
|------|-------|---------|--------|
| Login | `/admin/login` | Authentication | ✅ Working |
| Dashboard | `/admin/dashboard` | Admin home | ✅ Working |
| Pages Manager | `/admin/pages` | CRUD for Pages | ✅ Working |
| Products Manager | `/admin/products` | CRUD for Products | ✅ Working |
| Blog Manager | `/admin/blog` | CRUD for BlogPosts | ✅ Working |
| Portfolio Manager | `/admin/portfolio` | CRUD for Categories & Projects | ✅ Working |
| Homepage Editor | `/admin/homepage` | Edit homepage sections | ✅ Working |
| Menu Manager | `/admin/menu` | CRUD for MenuItems | ✅ Working |
| Settings | `/admin/settings` | Site configuration | ✅ Working |
| Upload Manager | `/admin/upload` | Image uploads | ✅ Working |
---
## 🔧 Backend Services
### Core Services
| Service | Purpose | Status |
|---------|---------|--------|
| **MongoDBService** | Generic CRUD for MongoDB | ✅ Working |
| **SlugService** | URL-friendly slug generation | ✅ Working |
| **ApiUploadController** | Image upload API | ✅ Working |
### Service Methods (MongoDBService)
-`GetAllAsync<T>(collectionName)` - Retrieve all documents
-`GetByIdAsync<T>(collectionName, id)` - Get single document
-`InsertAsync<T>(collectionName, document)` - Create document
-`UpdateAsync<T>(collectionName, id, document)` - Update document
-`DeleteAsync<T>(collectionName, id)` - Delete document
---
## 📁 File Structure
```
Sky_Art_Shop/
├── Controllers/ ✅ 17 controllers (all working)
│ ├── Public: HomeController, ShopController, PortfolioController,
│ │ BlogController, AboutController, ContactController, PageController
│ └── Admin: AdminController, AdminPagesController, AdminProductsController,
│ AdminBlogController, AdminPortfolioController, AdminHomepageController,
│ AdminMenuController, AdminSettingsController, AdminUploadController,
│ ApiUploadController
├── Models/ ✅ DatabaseModels.cs (all models defined)
├── Services/ ✅ MongoDBService.cs, SlugService.cs
├── Data/ ✅ ApplicationDbContext.cs (Identity)
├── Views/ ✅ 41 Razor views (organized by controller)
├── ViewComponents/ ✅ NavigationViewComponent, FooterPagesViewComponent
├── wwwroot/ ✅ Static assets
│ ├── assets/css/ ✅ main.css (organized & optimized)
│ ├── assets/js/ ✅ main.js, cart.js
│ ├── uploads/images/ ✅ 41 uploaded images
│ └── assets/images/ ✅ Placeholder images
└── Program.cs ✅ Configuration & database initialization
```
---
## ✨ Recent Features Implemented
### About Page Enhancements
-**Image Gallery**: Right sidebar with multiple images
-**Team Members Section**: Cards with photos, names, roles, and bios
-**Dynamic Content**: Editable from admin panel
-**Form Handling**: Manual parsing for complex collections (ImageGallery, TeamMembers)
### Upload System
-**API Endpoint**: `/api/upload/image` for AJAX uploads
-**File Validation**: Type (jpg, jpeg, png, gif, webp) & size (5MB max)
-**Storage**: /wwwroot/uploads/images/ with GUID filenames
-**Multiple Uploads**: Batch processing support
### UI/UX Improvements
-**Logo Integration**: Cat image in navbar (circular, no border)
-**Team Member Cards**: Information at top, photo at bottom, circular images
-**Responsive Design**: Cards max 300px width, centered grid layout
-**Proper Spacing**: Adjusted margins between content and images
---
## 🔐 Security
-**Authentication**: ASP.NET Core Identity
-**Authorization**: Role-based (Admin role required for admin pages)
-**CSRF Protection**: Anti-forgery tokens on all forms
-**File Upload Security**: Type and size validation
-**SQL Injection**: Protected by Entity Framework Core
-**NoSQL Injection**: Protected by MongoDB driver
---
## 🗄️ Database Connections
### MongoDB
**Connection String:** `mongodb://localhost:27017`
**Database:** `SkyArtShopDB`
**Status:** ✅ Connected and operational
### SQLite (Identity)
**Connection String:** `Data Source=identity.db`
**Purpose:** User authentication (ASP.NET Core Identity)
**Status:** ✅ Connected and operational
---
## 📝 Code Quality
### ✅ Organized
- Controllers follow single responsibility principle
- Services use dependency injection
- Consistent naming conventions
- Proper route attributes
### ✅ No Dead Code
- All controllers actively used
- All views mapped to controllers
- All services in use
### ✅ Communication Flow
```
Frontend (Razor Views)
Controllers (MVC)
Services (MongoDBService, SlugService)
MongoDB / SQLite
```
### ✅ Data Persistence
- All form data properly saved to MongoDB
- Image uploads stored in wwwroot/uploads/images/
- Complex collections (ImageGallery, TeamMembers) manually parsed and saved
- All CRUD operations tested and working
---
## ⚠️ Minor Issues Fixed
1. ✅ Hot reload crash - Fixed with clean rebuild
2. ✅ Model binding for collections - Fixed with manual form parsing
3. ✅ Null reference warning - Fixed with null-coalescing operators
4. ✅ Image gallery not saving - Fixed with IFormCollection parsing
5. ✅ Team members not persisting - Fixed with manual collection building
---
## 🚀 Performance
- ✅ Minimal console logging (can be removed for production)
- ✅ Efficient MongoDB queries
- ✅ Static file caching enabled
- ✅ Session management configured
- ✅ No N+1 query issues
---
## 📊 Statistics
- **Total Controllers:** 17
- **Total Views:** 41
- **MongoDB Collections:** 8
- **Uploaded Images:** 41
- **Menu Items:** 10
- **Homepage Sections:** 3
- **Portfolio Categories:** 4
- **Build Status:** ✅ Success (1 warning - non-critical)
---
## 🎯 System Health: EXCELLENT
All components are:
- ✅ Connected properly
- ✅ Communicating correctly
- ✅ Storing data in MongoDB
- ✅ Serving pages without errors
- ✅ Organized and maintainable
**No cleanup needed. System is production-ready.**

View File

@@ -1,64 +0,0 @@
using MongoDB.Driver;
using Microsoft.Extensions.Options;
namespace SkyArtShop.Services
{
public class MongoDBSettings
{
public string ConnectionString { get; set; } = string.Empty;
public string DatabaseName { get; set; } = string.Empty;
public Dictionary<string, string> Collections { get; set; } = new Dictionary<string, string>();
}
public class MongoDBService
{
private readonly IMongoDatabase _database;
private readonly MongoDBSettings _settings;
public MongoDBService(IOptions<MongoDBSettings> settings)
{
_settings = settings.Value;
var client = new MongoClient(_settings.ConnectionString);
_database = client.GetDatabase(_settings.DatabaseName);
}
public IMongoCollection<T> GetCollection<T>(string collectionName)
{
return _database.GetCollection<T>(collectionName);
}
// Helper methods for common operations
public async Task<List<T>> GetAllAsync<T>(string collectionName)
{
var collection = GetCollection<T>(collectionName);
return await collection.Find(_ => true).ToListAsync();
}
public async Task<T> GetByIdAsync<T>(string collectionName, string id)
{
var collection = GetCollection<T>(collectionName);
var filter = Builders<T>.Filter.Eq("_id", MongoDB.Bson.ObjectId.Parse(id));
return await collection.Find(filter).FirstOrDefaultAsync();
}
public async Task InsertAsync<T>(string collectionName, T document)
{
var collection = GetCollection<T>(collectionName);
await collection.InsertOneAsync(document);
}
public async Task UpdateAsync<T>(string collectionName, string id, T document)
{
var collection = GetCollection<T>(collectionName);
var filter = Builders<T>.Filter.Eq("_id", MongoDB.Bson.ObjectId.Parse(id));
await collection.ReplaceOneAsync(filter, document);
}
public async Task DeleteAsync<T>(string collectionName, string id)
{
var collection = GetCollection<T>(collectionName);
var filter = Builders<T>.Filter.Eq("_id", MongoDB.Bson.ObjectId.Parse(id));
await collection.DeleteOneAsync(filter);
}
}
}

View File

@@ -1,33 +0,0 @@
using System.Text.RegularExpressions;
namespace SkyArtShop.Services
{
public class SlugService
{
public string GenerateSlug(string text)
{
if (string.IsNullOrWhiteSpace(text))
return string.Empty;
// Convert to lowercase
var slug = text.ToLowerInvariant();
// Replace spaces with hyphens
slug = slug.Replace(" ", "-");
// Replace & with "and"
slug = slug.Replace("&", "and");
// Remove invalid characters
slug = Regex.Replace(slug, @"[^a-z0-9\-]", "");
// Replace multiple hyphens with single hyphen
slug = Regex.Replace(slug, @"-+", "-");
// Trim hyphens from start and end
slug = slug.Trim('-');
return slug;
}
}
}

View File

@@ -1,26 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>SkyArtShop</RootNamespace>
<DefaultItemExcludes>$(DefaultItemExcludes);_old_admin_backup\**</DefaultItemExcludes>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<CopyRazorGenerateFilesToPublishDirectory>true</CopyRazorGenerateFilesToPublishDirectory>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.23.1" />
<PackageReference Include="Microsoft.AspNetCore.Session" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -1,24 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SkyArtShop", "SkyArtShop.csproj", "{18789CFC-15BE-BCAD-678C-3D46964FB388}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{18789CFC-15BE-BCAD-678C-3D46964FB388}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18789CFC-15BE-BCAD-678C-3D46964FB388}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18789CFC-15BE-BCAD-678C-3D46964FB388}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18789CFC-15BE-BCAD-678C-3D46964FB388}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {245B1FFA-E45F-4311-AA3E-632F6507C697}
EndGlobalSection
EndGlobal

View File

@@ -1,22 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.ViewComponents
{
public class FooterPagesViewComponent : ViewComponent
{
private readonly MongoDBService _mongoService;
public FooterPagesViewComponent(MongoDBService mongoService)
{
_mongoService = mongoService;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var pages = await _mongoService.GetAllAsync<Page>("Pages");
return View(pages.Where(p => p.IsActive).OrderBy(p => p.PageName).ToList());
}
}
}

View File

@@ -1,40 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using SkyArtShop.Models;
using SkyArtShop.Services;
namespace SkyArtShop.ViewComponents
{
public class NavigationViewComponent : ViewComponent
{
private readonly MongoDBService _mongoService;
public NavigationViewComponent(MongoDBService mongoService)
{
_mongoService = mongoService;
}
public async Task<IViewComponentResult> InvokeAsync(string location = "navbar")
{
var menuItems = await _mongoService.GetAllAsync<MenuItem>("MenuItems");
if (location == "dropdown")
{
// For dropdown: must be Active AND ShowInDropdown
var dropdownItems = menuItems
.Where(m => m.IsActive && m.ShowInDropdown)
.OrderBy(m => m.DisplayOrder)
.ToList();
return View(dropdownItems);
}
else
{
// For navbar: must be Active AND ShowInNavbar
var navbarItems = menuItems
.Where(m => m.IsActive && m.ShowInNavbar)
.OrderBy(m => m.DisplayOrder)
.ToList();
return View(navbarItems);
}
}
}
}

View File

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

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

Some files were not shown because too many files have changed in this diff Show More