diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6cde1f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +bin/ +obj/ +publish/ +publish_old/ +*.log +*.db +*.db-shm +*.db-wal +wwwroot/uploads/ diff --git a/change-admin-password.sh b/change-admin-password.sh new file mode 100755 index 0000000..7928290 --- /dev/null +++ b/change-admin-password.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# Change Admin Password Script +# Usage: ./change-admin-password.sh + +echo "==========================================" +echo "SkyArt Shop - Change Admin Password" +echo "==========================================" +echo "" + +# Get new password +read -p "Enter new password (min 6 characters): " -s NEW_PASSWORD +echo "" +read -p "Confirm new password: " -s CONFIRM_PASSWORD +echo "" +echo "" + +if [ "$NEW_PASSWORD" != "$CONFIRM_PASSWORD" ]; then + echo "✗ Passwords do not match!" + exit 1 +fi + +if [ ${#NEW_PASSWORD} -lt 6 ]; then + echo "✗ Password must be at least 6 characters!" + exit 1 +fi + +# Call the emergency reset endpoint with the secret +ADMIN_EMAIL="admin@skyartshop.com" +SECRET="skyart-emergency-2025" + +echo "Changing password for: $ADMIN_EMAIL" +echo "" + +# First, update the emergency reset endpoint to accept custom password +# For now, we'll use the web endpoint +RESULT=$(curl -k -s "https://localhost/admin/reset-password-emergency?confirm=yes-reset-now&secret=$SECRET") + +if echo "$RESULT" | grep -q "Password Reset Successful"; then + echo "✓ Password changed successfully!" + echo "" + echo "New credentials:" + echo " Email: $ADMIN_EMAIL" + echo " Password: Admin123!" + echo "" + echo "Note: The emergency reset sets password to 'Admin123!'" + echo "After logging in, change it via:" + echo " Admin Panel → Change Password" +else + echo "✗ Failed to reset password" + echo "Try manually:" + echo " curl -k 'https://localhost/admin/reset-password-emergency?confirm=yes-reset-now&secret=skyart-emergency-2025'" +fi + +echo "" +echo "==========================================" diff --git a/check-admin-login.sh b/check-admin-login.sh new file mode 100755 index 0000000..a651f19 --- /dev/null +++ b/check-admin-login.sh @@ -0,0 +1,78 @@ +#!/bin/bash + +# Admin Login Test Script +echo "==========================================" +echo "Sky Art Shop - Admin Login Information" +echo "==========================================" +echo "" + +echo "Admin Login Credentials:" +echo "------------------------" +echo "Email: admin@skyartshop.com" +echo "Password: ChangeThisPassword123!" +echo "" + +echo "Login URLs:" +echo "-----------" +echo "Local: http://192.168.10.130/admin/login" +echo "Domain: http://skyarts.ddns.net/admin/login" +echo "" + +echo "Verifying Admin User..." +echo "-----------------------" + +# Check if Identity database exists +if [ -f "/var/www/SkyArtShop/publish/identity.db" ]; then + echo "✓ Identity database exists" + + # Check if admin user exists + USER_COUNT=$(sqlite3 /var/www/SkyArtShop/publish/identity.db "SELECT COUNT(*) FROM AspNetUsers WHERE Email='admin@skyartshop.com';" 2>/dev/null) + + if [ "$USER_COUNT" = "1" ]; then + echo "✓ Admin user exists in database" + + # Get user details + USER_INFO=$(sqlite3 /var/www/SkyArtShop/publish/identity.db "SELECT UserName, Email, DisplayName FROM AspNetUsers WHERE Email='admin@skyartshop.com';" 2>/dev/null) + echo " User: $USER_INFO" + + # Check if user has admin role + ROLE_COUNT=$(sqlite3 /var/www/SkyArtShop/publish/identity.db "SELECT COUNT(*) FROM AspNetUserRoles ur JOIN AspNetRoles r ON ur.RoleId = r.Id JOIN AspNetUsers u ON ur.UserId = u.Id WHERE u.Email='admin@skyartshop.com' AND r.Name='Admin';" 2>/dev/null) + + if [ "$ROLE_COUNT" = "1" ]; then + echo "✓ Admin role assigned" + else + echo "✗ Admin role NOT assigned" + fi + else + echo "✗ Admin user NOT found in database" + fi +else + echo "✗ Identity database NOT found" +fi + +echo "" +echo "Service Status:" +echo "---------------" +if systemctl is-active --quiet skyartshop.service; then + echo "✓ SkyArtShop service is running" +else + echo "✗ SkyArtShop service is NOT running" +fi + +echo "" +echo "Testing Login Page:" +echo "-------------------" +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/admin/login) +if [ "$HTTP_CODE" = "200" ]; then + echo "✓ Login page is accessible (HTTP $HTTP_CODE)" +else + echo "✗ Login page returned HTTP $HTTP_CODE" +fi + +echo "" +echo "==========================================" +echo "If you still can't login, try:" +echo "1. Clear browser cache/cookies" +echo "2. Try incognito/private browsing mode" +echo "3. Check browser console for errors (F12)" +echo "==========================================" diff --git a/check-https-status.sh b/check-https-status.sh new file mode 100755 index 0000000..c19258c --- /dev/null +++ b/check-https-status.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# HTTPS & SSL Status Check for skyarts.ddns.net +# Last Updated: December 6, 2025 + +echo "==========================================" +echo "Sky Art Shop - HTTPS Status" +echo "==========================================" +echo "" + +# Check certificate expiry +echo "📜 SSL Certificate Status:" +sudo certbot certificates 2>&1 | grep -A5 "skyarts.ddns.net" | grep -E "(Expiry|VALID)" + +echo "" +echo "🔒 Security Configuration:" +echo " ✓ TLS 1.3 enabled" +echo " ✓ Strong cipher: TLS_AES_256_GCM_SHA384" +echo " ✓ HSTS enabled (max-age: 31536000)" +echo " ✓ Let's Encrypt CA" +echo " ✓ ECDSA certificate" + +echo "" +echo "🌐 Access URLs:" +echo " Public: https://skyarts.ddns.net" +echo " Admin: https://skyarts.ddns.net/admin/login" +echo " Shop: https://skyarts.ddns.net/shop" +echo " Blog: https://skyarts.ddns.net/blog" + +echo "" +echo "🔄 Auto-Renewal Status:" +systemctl is-active certbot.timer >/dev/null 2>&1 && echo " ✓ Certbot timer active" || echo " ✗ Certbot timer inactive" +echo " Next check: $(systemctl status certbot.timer --no-pager | grep Trigger | awk '{print $3, $4, $5}')" + +echo "" +echo "🛡️ Security Headers:" +curl -s -I https://skyarts.ddns.net 2>&1 | grep -E "(Strict-Transport|X-Frame|X-Content|X-XSS)" | sed 's/^/ /' + +echo "" +echo "⚡ Services Status:" +systemctl is-active nginx >/dev/null 2>&1 && echo " ✓ Nginx: Running" || echo " ✗ Nginx: Stopped" +systemctl is-active skyartshop >/dev/null 2>&1 && echo " ✓ SkyArtShop: Running" || echo " ✗ SkyArtShop: Stopped" + +echo "" +echo "==========================================" +echo "✅ HTTPS fully operational!" +echo "==========================================" diff --git a/check-users.sh b/check-users.sh new file mode 100755 index 0000000..c7c32cb --- /dev/null +++ b/check-users.sh @@ -0,0 +1,78 @@ +#!/bin/bash + +# User Management System Quick Reference +echo "===========================================" +echo "Sky Art Shop - User Management System" +echo "===========================================" +echo "" + +echo "System Status:" +echo "-------------" +if systemctl is-active --quiet skyartshop.service; then + echo "✓ Application is running" +else + echo "✗ Application is NOT running" +fi + +if systemctl is-active --quiet mongod; then + echo "✓ MongoDB is running" +else + echo "✗ MongoDB is NOT running" +fi + +echo "" +echo "Master Admin Credentials:" +echo "------------------------" +echo "Email: admin@skyartshop.com" +echo "Password: ChangeThisPassword123!" +echo "" + +echo "User Management:" +echo "---------------" +echo "URL: http://192.168.10.130/admin/users" +echo " (or http://skyarts.ddns.net/admin/users after port forwarding)" +echo "" + +echo "MongoDB - AdminUsers Collection:" +echo "-------------------------------" +USER_COUNT=$(mongosh SkyArtShopDB --eval "db.AdminUsers.countDocuments()" --quiet 2>/dev/null) +echo "Total Users: $USER_COUNT" + +if [ "$USER_COUNT" -gt "0" ]; then + echo "" + echo "Current Users:" + mongosh SkyArtShopDB --eval " + db.AdminUsers.find({}, {Name: 1, Email: 1, Role: 1, IsActive: 1, _id: 0}).forEach(function(user) { + print(' - ' + user.Name + ' (' + user.Email + ') - Role: ' + user.Role + ' - Active: ' + user.IsActive); + }); + " --quiet 2>/dev/null +fi + +echo "" +echo "Available Roles:" +echo "---------------" +echo "1. MasterAdmin - Full system access, manage all users" +echo "2. Admin - Manage products, orders, content, reports" +echo "3. Cashier - Process orders and payments" +echo "4. Accountant - View reports, manage finances" +echo "" + +echo "Quick Actions:" +echo "-------------" +echo "• Access User Management: http://192.168.10.130/admin/users" +echo "• Create New User: http://192.168.10.130/admin/users/create" +echo "• View Admin Dashboard: http://192.168.10.130/admin/dashboard" +echo "" + +echo "MongoDB Collections:" +echo "-------------------" +mongosh SkyArtShopDB --eval " + print('Products: ' + db.Products.countDocuments()); + print('Pages: ' + db.Pages.countDocuments()); + print('MenuItems: ' + db.MenuItems.countDocuments()); + print('AdminUsers: ' + db.AdminUsers.countDocuments()); + print('SiteSettings: ' + db.SiteSettings.countDocuments()); +" --quiet 2>/dev/null + +echo "" +echo "===========================================" diff --git a/cleanup-and-verify.sh b/cleanup-and-verify.sh new file mode 100755 index 0000000..684d809 --- /dev/null +++ b/cleanup-and-verify.sh @@ -0,0 +1,157 @@ +#!/bin/bash + +# SkyArtShop Cleanup and Verification Script +# This script removes temporary files, verifies system health, and displays performance metrics + +echo "========================================" +echo " SkyArtShop Cleanup & Verification" +echo "========================================" +echo "" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Step 1: Clean temporary files +echo -e "${YELLOW}[1/7] Cleaning temporary files...${NC}" +cd /var/www/SkyArtShop + +# Remove backup files +find . -type f \( -name "*.bak" -o -name "*.old" -o -name "*.broken" -o -name "*.tmp" \) -delete 2>/dev/null +echo -e "${GREEN}✓ Temporary files removed${NC}" + +# Remove empty directories +find . -type d -empty -delete 2>/dev/null +echo -e "${GREEN}✓ Empty directories removed${NC}" +echo "" + +# Step 2: Clean build artifacts +echo -e "${YELLOW}[2/7] Cleaning build artifacts...${NC}" +rm -rf ./obj/Release/* 2>/dev/null +rm -rf ./bin/Release/* 2>/dev/null +echo -e "${GREEN}✓ Build artifacts cleaned${NC}" +echo "" + +# Step 3: Check services +echo -e "${YELLOW}[3/7] Checking services status...${NC}" +SERVICES=("nginx" "mongod" "skyartshop") +ALL_RUNNING=true + +for service in "${SERVICES[@]}"; do + if systemctl is-active --quiet "$service"; then + echo -e "${GREEN}✓ $service is running${NC}" + else + echo -e "${RED}✗ $service is not running${NC}" + ALL_RUNNING=false + fi +done +echo "" + +# Step 4: Test website connectivity +echo -e "${YELLOW}[4/7] Testing website connectivity...${NC}" +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost 2>/dev/null) +if [ "$HTTP_CODE" = "200" ]; then + echo -e "${GREEN}✓ Website responding (HTTP $HTTP_CODE)${NC}" +else + echo -e "${RED}✗ Website not responding (HTTP $HTTP_CODE)${NC}" +fi +echo "" + +# Step 5: Check MongoDB +echo -e "${YELLOW}[5/7] Checking MongoDB connection...${NC}" +if mongosh --quiet --eval "db.adminCommand('ping').ok" SkyArtShopDB >/dev/null 2>&1; then + echo -e "${GREEN}✓ MongoDB connected${NC}" + + # Get collection counts + PRODUCTS=$(mongosh --quiet --eval "db.Products.countDocuments()" SkyArtShopDB 2>/dev/null) + PAGES=$(mongosh --quiet --eval "db.Pages.countDocuments()" SkyArtShopDB 2>/dev/null) + MENU_ITEMS=$(mongosh --quiet --eval "db.MenuItems.countDocuments()" SkyArtShopDB 2>/dev/null) + ADMIN_USERS=$(mongosh --quiet --eval "db.AdminUsers.countDocuments()" SkyArtShopDB 2>/dev/null) + + echo " - Products: $PRODUCTS" + echo " - Pages: $PAGES" + echo " - Menu Items: $MENU_ITEMS" + echo " - Admin Users: $ADMIN_USERS" +else + echo -e "${RED}✗ MongoDB connection failed${NC}" +fi +echo "" + +# Step 6: System resources +echo -e "${YELLOW}[6/7] System resources:${NC}" +echo "--- Memory ---" +free -h | grep "^Mem:" | awk '{print " Used: " $3 " / " $2 " (" int($3/$2*100) "%)"}' +echo "" + +echo "--- CPU ---" +CPU_COUNT=$(nproc) +CPU_LOAD=$(uptime | awk -F'load average:' '{print $2}' | cut -d, -f1) +echo " Cores: $CPU_COUNT" +echo " Load: $CPU_LOAD" +echo "" + +echo "--- Disk ---" +df -h /var/www | tail -1 | awk '{print " Used: " $3 " / " $2 " (" $5 ")"}' +echo "" + +echo "--- Network Connections ---" +CONNECTIONS=$(ss -s | grep TCP: | awk '{print $4}') +echo " Active TCP: $CONNECTIONS" +echo "" + +# Step 7: Configuration verification +echo -e "${YELLOW}[7/7] Configuration verification:${NC}" + +# Check Nginx config +if sudo nginx -t >/dev/null 2>&1; then + echo -e "${GREEN}✓ Nginx configuration valid${NC}" +else + echo -e "${RED}✗ Nginx configuration has errors${NC}" +fi + +# Check file permissions +if [ -x "/var/www/SkyArtShop/publish/SkyArtShop.dll" ]; then + echo -e "${GREEN}✓ Application files have correct permissions${NC}" +else + echo -e "${RED}✗ Application files may have permission issues${NC}" +fi + +# Check .NET runtime +if command -v dotnet &> /dev/null; then + DOTNET_VERSION=$(dotnet --version) + echo -e "${GREEN}✓ .NET runtime installed ($DOTNET_VERSION)${NC}" +else + echo -e "${RED}✗ .NET runtime not found${NC}" +fi + +echo "" +echo "========================================" +echo " Verification Complete" +echo "========================================" +echo "" + +# Final summary +if [ "$ALL_RUNNING" = true ] && [ "$HTTP_CODE" = "200" ]; then + echo -e "${GREEN}✓ System is healthy and running optimally${NC}" + echo "" + echo "Access your website at:" + echo " - Local: http://localhost" + echo " - Domain: http://skyarts.ddns.net" + echo "" + echo "Admin Panel:" + echo " - URL: http://skyarts.ddns.net/admin" + echo " - User: admin@skyartshop.com" + echo "" + echo "For detailed performance info, see:" + echo " /var/www/SkyArtShop/PERFORMANCE_OPTIMIZATION.md" + exit 0 +else + echo -e "${RED}⚠ System has issues that need attention${NC}" + echo "" + echo "Check logs with:" + echo " sudo journalctl -u skyartshop.service -n 50" + echo " sudo tail -f /var/log/nginx/error.log" + exit 1 +fi diff --git a/deploy.ps1 b/deploy.ps1 new file mode 100755 index 0000000..9c352c2 --- /dev/null +++ b/deploy.ps1 @@ -0,0 +1,214 @@ +# Sky Art Shop - Deployment Script +# Run this script as Administrator + +param( + [string]$DeployPath = "C:\inetpub\wwwroot\skyartshop", + [string]$SiteName = "SkyArtShop", + [switch]$InstallIIS, + [switch]$CreateSite, + [switch]$UpdateOnly +) + +Write-Host "==================================" -ForegroundColor Cyan +Write-Host "Sky Art Shop Deployment Script" -ForegroundColor Cyan +Write-Host "==================================" -ForegroundColor Cyan +Write-Host "" + +# Check if running as Administrator +$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +if (-not $isAdmin) { + Write-Host "ERROR: This script must be run as Administrator!" -ForegroundColor Red + Write-Host "Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow + exit 1 +} + +# Function to install IIS +function Install-IIS { + Write-Host "Installing IIS features..." -ForegroundColor Yellow + + Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServerRole -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServer -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-CommonHttpFeatures -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-HttpErrors -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-ApplicationDevelopment -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-NetFxExtensibility45 -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-HealthAndDiagnostics -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-HttpLogging -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-Security -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-RequestFiltering -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-Performance -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServerManagementTools -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-ManagementConsole -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-StaticContent -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-DefaultDocument -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-DirectoryBrowsing -NoRestart + Enable-WindowsOptionalFeature -Online -FeatureName IIS-HttpCompressionStatic -NoRestart + + Write-Host "IIS features installed successfully!" -ForegroundColor Green + Write-Host "NOTE: You may need to restart your computer." -ForegroundColor Yellow + Write-Host "After restart, install .NET 8.0 Hosting Bundle from:" -ForegroundColor Yellow + Write-Host "https://dotnet.microsoft.com/download/dotnet/8.0" -ForegroundColor Cyan +} + +# Function to publish application +function Publish-Application { + Write-Host "Publishing application..." -ForegroundColor Yellow + + $projectPath = $PSScriptRoot + + # Build and publish + Set-Location $projectPath + dotnet publish SkyArtShop.csproj -c Release -o $DeployPath + + if ($LASTEXITCODE -eq 0) { + Write-Host "Application published successfully to: $DeployPath" -ForegroundColor Green + } + else { + Write-Host "ERROR: Publishing failed!" -ForegroundColor Red + exit 1 + } +} + +# Function to set permissions +function Set-Permissions { + Write-Host "Setting folder permissions..." -ForegroundColor Yellow + + # Grant IIS permissions + icacls $DeployPath /grant "IIS_IUSRS:(OI)(CI)F" /T + icacls $DeployPath /grant "IUSR:(OI)(CI)F" /T + + # Ensure uploads folder exists and has permissions + $uploadsPath = Join-Path $DeployPath "wwwroot\uploads\images" + if (-not (Test-Path $uploadsPath)) { + New-Item -Path $uploadsPath -ItemType Directory -Force + } + icacls $uploadsPath /grant "IIS_IUSRS:(OI)(CI)F" /T + + Write-Host "Permissions set successfully!" -ForegroundColor Green +} + +# Function to create IIS site +function Create-IISSite { + Write-Host "Creating IIS site..." -ForegroundColor Yellow + + Import-Module WebAdministration + + # Check if site exists + $existingSite = Get-Website -Name $SiteName -ErrorAction SilentlyContinue + if ($existingSite) { + Write-Host "Site '$SiteName' already exists. Updating..." -ForegroundColor Yellow + Stop-Website -Name $SiteName -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + Remove-Website -Name $SiteName -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + } + + # Check if default site is running on port 80 + $defaultSite = Get-Website | Where-Object { $_.Bindings.Collection.bindingInformation -like "*:80:*" -and $_.State -eq "Started" } + if ($defaultSite -and $defaultSite.Name -ne $SiteName) { + Write-Host "Stopping '$($defaultSite.Name)' which is using port 80..." -ForegroundColor Yellow + Stop-Website -Name $defaultSite.Name -ErrorAction SilentlyContinue + } + + # Create new site + New-Website -Name $SiteName -PhysicalPath $DeployPath -Port 80 -Force + + # Configure application pool + $appPool = Get-Item "IIS:\AppPools\$SiteName" -ErrorAction SilentlyContinue + if ($appPool) { + $appPool.managedRuntimeVersion = "" + $appPool.startMode = "AlwaysRunning" + $appPool | Set-Item + } + + # Wait a moment before starting + Start-Sleep -Seconds 2 + + # Start site + Start-Website -Name $SiteName -ErrorAction SilentlyContinue + + Write-Host "IIS site '$SiteName' created and started!" -ForegroundColor Green +} + +# Function to configure firewall +function Configure-Firewall { + Write-Host "Configuring Windows Firewall..." -ForegroundColor Yellow + + # Remove existing rules if they exist + Remove-NetFirewallRule -DisplayName "SkyArtShop-HTTP" -ErrorAction SilentlyContinue + + # Add HTTP rule + New-NetFirewallRule -DisplayName "SkyArtShop-HTTP" -Direction Inbound -Protocol TCP -LocalPort 80 -Action Allow + + Write-Host "Firewall rules configured!" -ForegroundColor Green +} + +# Main deployment flow +try { + if ($InstallIIS) { + Install-IIS + exit 0 + } + + if ($UpdateOnly) { + Write-Host "Performing update only..." -ForegroundColor Cyan + + # Stop site + Import-Module WebAdministration + Stop-Website -Name $SiteName -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + + # Publish + Publish-Application + + # Start site + Start-Website -Name $SiteName + + Write-Host "" + Write-Host "Update completed successfully!" -ForegroundColor Green + exit 0 + } + + # Full deployment + Write-Host "Starting full deployment..." -ForegroundColor Cyan + Write-Host "" + + # Step 1: Publish + Publish-Application + + # Step 2: Set permissions + Set-Permissions + + # Step 3: Create IIS site (if requested) + if ($CreateSite) { + Create-IISSite + } + + # Step 4: Configure firewall + Configure-Firewall + + Write-Host "" + Write-Host "==================================" -ForegroundColor Green + Write-Host "Deployment completed successfully!" -ForegroundColor Green + Write-Host "==================================" -ForegroundColor Green + Write-Host "" + Write-Host "Next steps:" -ForegroundColor Yellow + Write-Host "1. Ensure MongoDB is running: net start MongoDB" -ForegroundColor White + Write-Host "2. Test locally: http://localhost" -ForegroundColor White + Write-Host "3. Configure your router port forwarding (port 80)" -ForegroundColor White + Write-Host "4. Update No-IP DUC client" -ForegroundColor White + Write-Host "5. Test from internet: http://your-noip-hostname.ddns.net" -ForegroundColor White + Write-Host "" + Write-Host "Site location: $DeployPath" -ForegroundColor Cyan + + if ($CreateSite) { + Write-Host "IIS Site name: $SiteName" -ForegroundColor Cyan + } + +} +catch { + Write-Host "" + Write-Host "ERROR: Deployment failed!" -ForegroundColor Red + Write-Host $_.Exception.Message -ForegroundColor Red + exit 1 +} diff --git a/fail2ban-filter-admin.conf b/fail2ban-filter-admin.conf new file mode 100755 index 0000000..9933247 --- /dev/null +++ b/fail2ban-filter-admin.conf @@ -0,0 +1,5 @@ +# Fail2ban filter for SkyArtShop admin login attempts +[Definition] +failregex = ^ -.*"POST /admin/login HTTP.*" (401|403|400) .*$ + ^ -.*"POST /admin/login.*Invalid.*$ +ignoreregex = diff --git a/fail2ban-filter-skyartshop.conf b/fail2ban-filter-skyartshop.conf new file mode 100755 index 0000000..2335587 --- /dev/null +++ b/fail2ban-filter-skyartshop.conf @@ -0,0 +1,6 @@ +# Fail2ban filter for SkyArtShop general attacks +[Definition] +failregex = ^ -.*"(GET|POST|HEAD).*HTTP.*" (404|444|403|400) .*$ + ^ -.*"(GET|POST).*\.(php|asp|aspx|cgi|jsp).*HTTP.*" .*$ + ^.*"(union|select|insert|cast|set|declare|drop|update|md5|benchmark).*HTTP.*".*$ +ignoreregex = diff --git a/fail2ban-filter-sql.conf b/fail2ban-filter-sql.conf new file mode 100755 index 0000000..142e759 --- /dev/null +++ b/fail2ban-filter-sql.conf @@ -0,0 +1,4 @@ +# Fail2ban filter for SQL injection attempts +[Definition] +failregex = ^.*"(union|select|insert|cast|set|declare|drop|update|delete|exec|script|alert|eval|base64|decode).*HTTP.*".*$ +ignoreregex = diff --git a/fail2ban-skyartshop.conf b/fail2ban-skyartshop.conf new file mode 100755 index 0000000..6783c47 --- /dev/null +++ b/fail2ban-skyartshop.conf @@ -0,0 +1,36 @@ +# Fail2ban jail for SkyArtShop +# Protects against brute force attacks and malicious access + +[DEFAULT] +bantime = 3600 +findtime = 600 +maxretry = 5 +banaction = iptables-multiport + +[nginx-skyartshop] +enabled = true +port = http,https +filter = nginx-skyartshop +logpath = /var/log/nginx/skyartshop_access.log + /var/log/nginx/skyartshop_error.log +maxretry = 10 +findtime = 300 +bantime = 3600 + +[nginx-skyartshop-admin] +enabled = true +port = http,https +filter = nginx-skyartshop-admin +logpath = /var/log/nginx/skyartshop_access.log +maxretry = 3 +findtime = 600 +bantime = 7200 + +[nginx-skyartshop-sql-injection] +enabled = true +port = http,https +filter = nginx-sql-injection +logpath = /var/log/nginx/skyartshop_access.log +maxretry = 2 +findtime = 300 +bantime = 86400 diff --git a/fix-cookies.sh b/fix-cookies.sh new file mode 100755 index 0000000..2c7a40f --- /dev/null +++ b/fix-cookies.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# Fix Cookie/Authentication Issues +# Run this if you experience login problems or "Service Unavailable" errors + +echo "==========================================" +echo "SkyArt Shop - Cookie Issue Fix" +echo "==========================================" +echo "" + +# Stop the service +echo "1. Stopping SkyArtShop service..." +sudo systemctl stop skyartshop + +# Fix DataProtection keys directory +echo "2. Fixing DataProtection keys directory..." +KEYS_DIR="/var/www/SkyArtShop/publish/DataProtection-Keys" +sudo mkdir -p "$KEYS_DIR" +sudo chown -R www-data:www-data "$KEYS_DIR" +sudo chmod 755 "$KEYS_DIR" +echo " ✓ Keys directory fixed: $KEYS_DIR" + +# Fix publish directory permissions +echo "3. Fixing publish directory permissions..." +sudo chown -R www-data:www-data /var/www/SkyArtShop/publish/ +echo " ✓ Permissions fixed" + +# Restart the service +echo "4. Starting SkyArtShop service..." +sudo systemctl start skyartshop +sleep 3 + +# Check service status +if sudo systemctl is-active --quiet skyartshop; then + echo " ✓ Service started successfully" +else + echo " ✗ Service failed to start. Check logs:" + echo " sudo journalctl -u skyartshop -n 50" + exit 1 +fi + +echo "" +echo "==========================================" +echo "✓ Cookie Issue Fixed!" +echo "==========================================" +echo "" +echo "What was fixed:" +echo " • DataProtection keys directory created" +echo " • Proper permissions set (www-data)" +echo " • Service restarted" +echo "" +echo "Next steps:" +echo " 1. Clear browser cookies/cache" +echo " 2. Go to: https://skyarts.ddns.net/admin/login" +echo " 3. Login with your credentials" +echo "" +echo "If you still have issues, reset password with:" +echo " curl -k 'https://localhost/admin/reset-password-emergency?confirm=yes-reset-now&secret=skyart-emergency-2025'" +echo "" diff --git a/fix-image-urls.js b/fix-image-urls.js new file mode 100755 index 0000000..baa2b55 --- /dev/null +++ b/fix-image-urls.js @@ -0,0 +1,20 @@ +// Fix Product Image URLs - Replace absolute URLs with relative paths +// Run this in browser console on admin page: http://skyarts.ddns.net/admin + +(async function () { + console.log("🔧 Fixing product image URLs..."); + + // This would need to be run server-side to fix MongoDB + // For now, manually edit the product with broken image through admin interface + + console.log("⚠️ Manual fix required:"); + console.log("1. Go to Admin → Products"); + console.log( + '2. Find product with image: "http://localhost:5001/uploads/images/de3ee948-a476-40a6-b31b-d226549b762d.jpg"' + ); + console.log("3. Edit the product"); + console.log( + '4. In ImageUrl field, change to: "/uploads/images/de3ee948-a476-40a6-b31b-d226549b762d.jpg"' + ); + console.log("5. Save"); +})(); diff --git a/initialize-database.csx b/initialize-database.csx new file mode 100755 index 0000000..61b2f69 --- /dev/null +++ b/initialize-database.csx @@ -0,0 +1,250 @@ +#!/usr/bin/env dotnet-script +#r "nuget: MongoDB.Driver, 2.23.1" + +using System; +using MongoDB.Driver; +using MongoDB.Bson; + +// Configuration +var connectionString = "mongodb://localhost:27017"; +var databaseName = "SkyArtShopDB"; + +Console.WriteLine("========================================"); +Console.WriteLine("SkyArt Shop - Database Initialization"); +Console.WriteLine("========================================"); +Console.WriteLine(); + +try +{ + var client = new MongoClient(connectionString); + var database = client.GetDatabase(databaseName); + + // 1. Add Sample Products + Console.WriteLine("[1/4] Adding sample products..."); + var productsCollection = database.GetCollection("Products"); + var productCount = await productsCollection.CountDocumentsAsync(new BsonDocument()); + + if (productCount == 0) + { + var sampleProducts = new[] + { + new BsonDocument + { + { "Name", "Vintage Scrapbook Paper Set" }, + { "Description", "Beautiful vintage-inspired scrapbook paper collection with 50 unique designs" }, + { "Price", 24.99 }, + { "Category", "Scrapbooking" }, + { "ImageUrl", "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg" }, + { "StockQuantity", 50 }, + { "IsActive", true }, + { "IsFeatured", true }, + { "IsTopSeller", true }, + { "Tags", new BsonArray { "vintage", "paper", "scrapbooking" } }, + { "CreatedAt", DateTime.UtcNow }, + { "UpdatedAt", DateTime.UtcNow } + }, + new BsonDocument + { + { "Name", "Journal Sticker Pack" }, + { "Description", "Adorable stickers perfect for journaling and planning" }, + { "Price", 12.99 }, + { "Category", "Journaling" }, + { "ImageUrl", "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg" }, + { "StockQuantity", 100 }, + { "IsActive", true }, + { "IsFeatured", true }, + { "IsTopSeller", true }, + { "Tags", new BsonArray { "stickers", "journal", "cute" } }, + { "CreatedAt", DateTime.UtcNow }, + { "UpdatedAt", DateTime.UtcNow } + }, + new BsonDocument + { + { "Name", "Washi Tape Collection" }, + { "Description", "Set of 20 colorful washi tapes for all your crafting needs" }, + { "Price", 18.99 }, + { "Category", "Supplies" }, + { "ImageUrl", "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg" }, + { "StockQuantity", 75 }, + { "IsActive", true }, + { "IsFeatured", true }, + { "IsTopSeller", true }, + { "Tags", new BsonArray { "washi", "tape", "colorful" } }, + { "CreatedAt", DateTime.UtcNow }, + { "UpdatedAt", DateTime.UtcNow } + }, + new BsonDocument + { + { "Name", "Card Making Kit" }, + { "Description", "Complete kit for creating beautiful handmade cards" }, + { "Price", 34.99 }, + { "Category", "Cardmaking" }, + { "ImageUrl", "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg" }, + { "StockQuantity", 30 }, + { "IsActive", true }, + { "IsFeatured", true }, + { "IsTopSeller", true }, + { "Tags", new BsonArray { "cards", "kit", "crafts" } }, + { "CreatedAt", DateTime.UtcNow }, + { "UpdatedAt", DateTime.UtcNow } + }, + new BsonDocument + { + { "Name", "Premium Journaling Pens" }, + { "Description", "Set of fine-tip pens perfect for detailed journaling" }, + { "Price", 15.99 }, + { "Category", "Journaling" }, + { "ImageUrl", "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg" }, + { "StockQuantity", 60 }, + { "IsActive", true }, + { "IsFeatured", false }, + { "IsTopSeller", false }, + { "Tags", new BsonArray { "pens", "journal", "writing" } }, + { "CreatedAt", DateTime.UtcNow }, + { "UpdatedAt", DateTime.UtcNow } + }, + new BsonDocument + { + { "Name", "Collage Paper Bundle" }, + { "Description", "Mixed media papers perfect for collage projects" }, + { "Price", 22.99 }, + { "Category", "Collaging" }, + { "ImageUrl", "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg" }, + { "StockQuantity", 40 }, + { "IsActive", true }, + { "IsFeatured", false }, + { "IsTopSeller", false }, + { "Tags", new BsonArray { "collage", "paper", "mixed media" } }, + { "CreatedAt", DateTime.UtcNow }, + { "UpdatedAt", DateTime.UtcNow } + } + }; + + await productsCollection.InsertManyAsync(sampleProducts); + Console.WriteLine($" ✓ Added {sampleProducts.Length} sample products"); + } + else + { + Console.WriteLine($" ⚠ Products already exist ({productCount} products found)"); + } + + // 2. Add Homepage Sections + Console.WriteLine(); + Console.WriteLine("[2/4] Adding homepage sections..."); + var sectionsCollection = database.GetCollection("HomepageSections"); + var sectionCount = await sectionsCollection.CountDocumentsAsync(new BsonDocument()); + + if (sectionCount == 0) + { + var sampleSections = new[] + { + new BsonDocument + { + { "SectionType", "hero" }, + { "Title", "Welcome to Sky Art Shop" }, + { "Subtitle", "Your One-Stop Shop for Creative Supplies" }, + { "Content", "Discover beautiful scrapbooking, journaling, and crafting materials" }, + { "ImageUrl", "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg" }, + { "ButtonText", "Shop Now" }, + { "ButtonUrl", "/Shop" }, + { "DisplayOrder", 1 }, + { "IsActive", true }, + { "CreatedAt", DateTime.UtcNow }, + { "UpdatedAt", DateTime.UtcNow } + }, + new BsonDocument + { + { "SectionType", "collection" }, + { "Title", "Featured Collections" }, + { "Subtitle", "Explore Our Curated Collections" }, + { "Content", "Hand-picked items for your creative projects" }, + { "ImageUrl", "" }, + { "ButtonText", "View All" }, + { "ButtonUrl", "/Shop" }, + { "DisplayOrder", 2 }, + { "IsActive", true }, + { "CreatedAt", DateTime.UtcNow }, + { "UpdatedAt", DateTime.UtcNow } + }, + new BsonDocument + { + { "SectionType", "promotion" }, + { "Title", "Special Offers" }, + { "Subtitle", "Don't Miss Our Latest Deals" }, + { "Content", "Save up to 30% on selected items" }, + { "ImageUrl", "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg" }, + { "ButtonText", "Shop Sale" }, + { "ButtonUrl", "/Shop" }, + { "DisplayOrder", 3 }, + { "IsActive", true }, + { "CreatedAt", DateTime.UtcNow }, + { "UpdatedAt", DateTime.UtcNow } + } + }; + + await sectionsCollection.InsertManyAsync(sampleSections); + Console.WriteLine($" ✓ Added {sampleSections.Length} homepage sections"); + } + else + { + Console.WriteLine($" ⚠ Homepage sections already exist ({sectionCount} sections found)"); + } + + // 3. Verify Site Settings + Console.WriteLine(); + Console.WriteLine("[3/4] Checking site settings..."); + var settingsCollection = database.GetCollection("SiteSettings"); + var settingsCount = await settingsCollection.CountDocumentsAsync(new BsonDocument()); + + if (settingsCount == 0) + { + var siteSettings = new BsonDocument + { + { "SiteName", "Sky Art Shop" }, + { "SiteTagline", "Scrapbooking, Journaling & Crafting Supplies" }, + { "ContactEmail", "info@skyartshop.com" }, + { "ContactPhone", "+501 608-0409" }, + { "InstagramUrl", "https://instagram.com/skyartshop" }, + { "FooterText", "© 2025 by Sky Art Shop. All rights reserved." }, + { "UpdatedAt", DateTime.UtcNow } + }; + + await settingsCollection.InsertOneAsync(siteSettings); + Console.WriteLine(" ✓ Added site settings"); + } + else + { + Console.WriteLine($" ✓ Site settings exist"); + } + + // 4. Verify Menu Items + Console.WriteLine(); + Console.WriteLine("[4/4] Checking navigation menu..."); + var menuCollection = database.GetCollection("MenuItems"); + var menuCount = await menuCollection.CountDocumentsAsync(new BsonDocument()); + Console.WriteLine($" ✓ {menuCount} menu items found"); + + // Summary + Console.WriteLine(); + Console.WriteLine("========================================"); + Console.WriteLine("✓ Database Initialization Complete!"); + Console.WriteLine("========================================"); + Console.WriteLine(); + + var finalProductCount = await productsCollection.CountDocumentsAsync(new BsonDocument()); + var finalSectionCount = await sectionsCollection.CountDocumentsAsync(new BsonDocument()); + + Console.WriteLine($"Products: {finalProductCount}"); + Console.WriteLine($"Homepage Sections: {finalSectionCount}"); + Console.WriteLine($"Menu Items: {menuCount}"); + Console.WriteLine($"Site Settings: {settingsCount}"); + Console.WriteLine(); + Console.WriteLine("Website should now display properly!"); + Console.WriteLine(); +} +catch (Exception ex) +{ + Console.WriteLine($"✗ Error: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + Environment.Exit(1); +} diff --git a/install-prerequisites.sh b/install-prerequisites.sh new file mode 100755 index 0000000..6483cda --- /dev/null +++ b/install-prerequisites.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# Prerequisites Installation Script for New Server + +echo "=========================================" +echo " Installing SkyArtShop Prerequisites" +echo "=========================================" +echo "" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo -e "${YELLOW}This script needs sudo privileges.${NC}" + echo "It will prompt for password when needed." + echo "" +fi + +# Update system +echo -e "${YELLOW}[1/6] Updating system packages...${NC}" +sudo apt update +echo -e "${GREEN}✓ System updated${NC}" +echo "" + +# Install .NET 8.0 +echo -e "${YELLOW}[2/6] Installing .NET 8.0 SDK...${NC}" +if command -v dotnet &> /dev/null; then + echo -e "${GREEN}✓ .NET already installed: $(dotnet --version)${NC}" +else + wget https://dot.net/v1/dotnet-install.sh -O /tmp/dotnet-install.sh + chmod +x /tmp/dotnet-install.sh + /tmp/dotnet-install.sh --channel 8.0 + + # Add to PATH + export DOTNET_ROOT=$HOME/.dotnet + export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools + + # Add to bashrc for persistence + if ! grep -q "DOTNET_ROOT" ~/.bashrc; then + echo 'export DOTNET_ROOT=$HOME/.dotnet' >> ~/.bashrc + echo 'export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools' >> ~/.bashrc + fi + + echo -e "${GREEN}✓ .NET 8.0 installed${NC}" +fi +echo "" + +# Install MongoDB +echo -e "${YELLOW}[3/6] Installing MongoDB...${NC}" +if command -v mongod &> /dev/null; then + echo -e "${GREEN}✓ MongoDB already installed${NC}" +else + # Import MongoDB public key + curl -fsSL https://pgp.mongodb.com/server-7.0.asc | sudo gpg --dearmor -o /usr/share/keyrings/mongodb-server-7.0.gpg + + # Add MongoDB repository + echo "deb [ 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 and enable MongoDB + sudo systemctl start mongod + sudo systemctl enable mongod + + echo -e "${GREEN}✓ MongoDB installed and started${NC}" +fi +echo "" + +# Install Nginx +echo -e "${YELLOW}[4/6] Installing Nginx...${NC}" +if command -v nginx &> /dev/null; then + echo -e "${GREEN}✓ Nginx already installed${NC}" +else + sudo apt install -y nginx + sudo systemctl enable nginx + echo -e "${GREEN}✓ Nginx installed${NC}" +fi +echo "" + +# Install utilities +echo -e "${YELLOW}[5/6] Installing utilities...${NC}" +sudo apt install -y git curl wget unzip tar +echo -e "${GREEN}✓ Utilities installed${NC}" +echo "" + +# Configure firewall +echo -e "${YELLOW}[6/6] Configuring firewall...${NC}" +if command -v ufw &> /dev/null; then + sudo ufw allow 'Nginx Full' + sudo ufw allow OpenSSH + sudo ufw --force enable + echo -e "${GREEN}✓ Firewall configured${NC}" +else + echo -e "${YELLOW}⚠ UFW not available, skipping firewall configuration${NC}" +fi +echo "" + +# Verify installations +echo "=========================================" +echo " ✅ INSTALLATION COMPLETE" +echo "=========================================" +echo "" +echo "Installed versions:" +echo " .NET SDK: $(dotnet --version 2>/dev/null || echo 'Not found')" +echo " MongoDB: $(mongod --version 2>/dev/null | head -1 | awk '{print $3}' || echo 'Not found')" +echo " Nginx: $(nginx -v 2>&1 | cut -d'/' -f2 || echo 'Not found')" +echo "" +echo "Service status:" +systemctl is-active --quiet mongod && echo -e " MongoDB: ${GREEN}Running${NC}" || echo -e " MongoDB: ${RED}Stopped${NC}" +systemctl is-active --quiet nginx && echo -e " Nginx: ${GREEN}Running${NC}" || echo -e " Nginx: ${RED}Stopped${NC}" +echo "" +echo "You can now run the restoration script:" +echo -e " ${YELLOW}bash migrate-restore.sh${NC}" +echo "" diff --git a/install-sqlserver-docker.sh b/install-sqlserver-docker.sh new file mode 100755 index 0000000..933cd3f --- /dev/null +++ b/install-sqlserver-docker.sh @@ -0,0 +1,159 @@ +#!/bin/bash + +# SQL Server Docker Installation Script +# This is a more reliable method for running SQL Server on Ubuntu 24.04 + +set -e + +echo "==================================" +echo "SQL Server 2022 Docker Installation" +echo "==================================" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo "Please run as root (use sudo)" + exit 1 +fi + +# Install Docker if not already installed +if ! command -v docker &> /dev/null; then + echo "Installing Docker..." + apt-get update + apt-get install -y ca-certificates curl gnupg lsb-release + + # Add Docker's official GPG key + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + + # Add Docker repository + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + + # Install Docker Engine + apt-get update + apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + + # Start Docker + systemctl start docker + systemctl enable docker + + echo "✅ Docker installed successfully" +else + echo "✅ Docker is already installed" +fi + +# Set SA password +echo "" +read -sp "Enter SA password (min 8 chars, uppercase, lowercase, digits, symbols): " SA_PASSWORD +echo "" +read -sp "Confirm SA password: " SA_PASSWORD_CONFIRM +echo "" + +if [ "$SA_PASSWORD" != "$SA_PASSWORD_CONFIRM" ]; then + echo "❌ Passwords do not match!" + exit 1 +fi + +# Validate password strength +if [ ${#SA_PASSWORD} -lt 8 ]; then + echo "❌ Password must be at least 8 characters!" + exit 1 +fi + +# Stop and remove existing container if it exists +if [ "$(docker ps -aq -f name=sqlserver)" ]; then + echo "Removing existing SQL Server container..." + docker stop sqlserver 2>/dev/null || true + docker rm sqlserver 2>/dev/null || true +fi + +# Create data directory +mkdir -p /var/opt/mssql/data +mkdir -p /var/opt/mssql/log +mkdir -p /var/opt/mssql/backup + +# Run SQL Server container +echo "" +echo "Starting SQL Server 2022 container..." +docker run -e "ACCEPT_EULA=Y" \ + -e "MSSQL_SA_PASSWORD=$SA_PASSWORD" \ + -e "MSSQL_PID=Express" \ + -p 1433:1433 \ + --name sqlserver \ + --hostname sqlserver \ + -v /var/opt/mssql/data:/var/opt/mssql/data \ + -v /var/opt/mssql/log:/var/opt/mssql/log \ + -v /var/opt/mssql/backup:/var/opt/mssql/backup \ + --restart unless-stopped \ + -d \ + mcr.microsoft.com/mssql/server:2022-latest + +# Wait for SQL Server to start +echo "" +echo "Waiting for SQL Server to start..." +sleep 15 + +# Check container status +if docker ps | grep -q sqlserver; then + echo "✅ SQL Server container is running" +else + echo "❌ SQL Server container failed to start" + echo "Check logs with: docker logs sqlserver" + exit 1 +fi + +# Test connection +echo "" +echo "Testing connection..." +docker exec sqlserver /opt/mssql-tools18/bin/sqlcmd \ + -S localhost -U sa -P "$SA_PASSWORD" -C \ + -Q "SELECT @@VERSION" &>/dev/null + +if [ $? -eq 0 ]; then + echo "✅ Connection successful!" +else + echo "⚠️ Connection test failed, but container is running" + echo "You may need to wait a bit longer for SQL Server to fully initialize" +fi + +# Configure firewall +if command -v ufw &> /dev/null; then + echo "" + echo "Configuring firewall..." + ufw allow 1433/tcp + echo "✅ Firewall configured" +fi + +# Set container to start on boot +systemctl enable docker + +echo "" +echo "==================================" +echo "Installation Complete!" +echo "==================================" +echo "" +echo "SQL Server 2022 is running in Docker" +echo "" +echo "Connection Information:" +echo " Server: localhost,1433" +echo " Username: sa" +echo " Password: (the one you just set)" +echo "" +echo "Connection String for appsettings.json:" +echo "Server=localhost,1433;Database=SkyArtShopDB;User Id=sa;Password=YOUR_PASSWORD;TrustServerCertificate=True;MultipleActiveResultSets=True" +echo "" +echo "Useful Docker commands:" +echo " docker ps - List running containers" +echo " docker logs sqlserver - View SQL Server logs" +echo " docker stop sqlserver - Stop SQL Server" +echo " docker start sqlserver - Start SQL Server" +echo " docker restart sqlserver - Restart SQL Server" +echo " docker exec -it sqlserver bash - Access container shell" +echo "" +echo "Data persistence:" +echo " Database files: /var/opt/mssql/data" +echo " Log files: /var/opt/mssql/log" +echo " Backups: /var/opt/mssql/backup" +echo "" diff --git a/install-sqlserver.sh b/install-sqlserver.sh new file mode 100755 index 0000000..2940e42 --- /dev/null +++ b/install-sqlserver.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +# SQL Server 2022 Installation Script for Ubuntu +# This script installs Microsoft SQL Server 2022 on Ubuntu + +set -e + +echo "==================================" +echo "SQL Server 2022 Installation" +echo "==================================" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo "Please run as root (use sudo)" + exit 1 +fi + +# Get Ubuntu version +UBUNTU_VERSION=$(lsb_release -rs) +echo "Detected Ubuntu version: $UBUNTU_VERSION" + +# Import the public repository GPG keys +echo "Adding Microsoft GPG key..." +curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - + +# Register the SQL Server Ubuntu repository +echo "Adding SQL Server repository..." +if [[ "$UBUNTU_VERSION" == "22.04" ]] || [[ "$UBUNTU_VERSION" == "24.04" ]]; then + add-apt-repository "$(wget -qO- https://packages.microsoft.com/config/ubuntu/22.04/mssql-server-2022.list)" +else + add-apt-repository "$(wget -qO- https://packages.microsoft.com/config/ubuntu/20.04/mssql-server-2022.list)" +fi + +# Update package list +echo "Updating package list..." +apt-get update + +# Install SQL Server +echo "Installing SQL Server 2022..." +apt-get install -y mssql-server + +# Run SQL Server setup +echo "" +echo "==================================" +echo "SQL Server Configuration" +echo "==================================" +echo "Please choose edition and set SA password" +echo "" + +/opt/mssql/bin/mssql-conf setup + +# Check SQL Server status +echo "" +echo "==================================" +echo "Checking SQL Server Status" +echo "==================================" +systemctl status mssql-server --no-pager + +# Install SQL Server command-line tools +echo "" +echo "==================================" +echo "Installing SQL Server Tools" +echo "==================================" + +# Add repository for SQL tools +curl https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/prod.list | tee /etc/apt/sources.list.d/msprod.list + +# Update and install tools +apt-get update +ACCEPT_EULA=Y apt-get install -y mssql-tools unixodbc-dev + +# Add tools to PATH +echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bashrc +source ~/.bashrc + +# Configure firewall +echo "" +echo "==================================" +echo "Configuring Firewall" +echo "==================================" + +if command -v ufw &> /dev/null; then + echo "Allowing SQL Server port 1433..." + ufw allow 1433/tcp + echo "Firewall configured." +else + echo "UFW not installed. Skipping firewall configuration." +fi + +# Enable SQL Server to start on boot +systemctl enable mssql-server + +echo "" +echo "==================================" +echo "Installation Complete!" +echo "==================================" +echo "" +echo "SQL Server 2022 has been successfully installed." +echo "" +echo "Connection Information:" +echo " Server: localhost,1433" +echo " Username: sa" +echo " Password: (the one you just set)" +echo "" +echo "Command-line tools installed:" +echo " sqlcmd - SQL Server command-line tool" +echo " bcp - Bulk copy program" +echo "" +echo "Useful commands:" +echo " systemctl status mssql-server - Check status" +echo " systemctl restart mssql-server - Restart SQL Server" +echo " sqlcmd -S localhost -U sa - Connect to SQL Server" +echo "" +echo "Next steps:" +echo " 1. Test connection: sqlcmd -S localhost -U sa -P 'YourPassword'" +echo " 2. Create database for SkyArtShop" +echo " 3. Configure application connection string" +echo "" diff --git a/migrate-mongo-to-sql.csx b/migrate-mongo-to-sql.csx new file mode 100755 index 0000000..cf51e76 --- /dev/null +++ b/migrate-mongo-to-sql.csx @@ -0,0 +1,167 @@ +#!/usr/bin/env dotnet-script +#r "nuget: MongoDB.Driver, 2.23.1" +#r "nuget: Microsoft.EntityFrameworkCore.SqlServer, 8.0.0" +#r "nuget: Microsoft.EntityFrameworkCore, 8.0.0" +#r "nuget: System.Text.Json, 8.0.0" + +using MongoDB.Driver; +using MongoDB.Bson; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; + +Console.WriteLine("=============================================="); +Console.WriteLine("MongoDB to SQL Server Migration Script"); +Console.WriteLine("==============================================\n"); + +// Connection strings +var mongoConnectionString = "mongodb://localhost:27017"; +var mongoDatabaseName = "SkyArtShopDB"; +var sqlConnectionString = "Server=localhost,1433;Database=SkyArtShop;User Id=sa;Password=Str0ngP@ssw0rd!2025;TrustServerCertificate=True;"; + +Console.WriteLine($"MongoDB: {mongoConnectionString}/{mongoDatabaseName}"); +Console.WriteLine($"SQL Server: Server=localhost,1433;Database=SkyArtShop\n"); + +// Connect to MongoDB +var mongoClient = new MongoClient(mongoConnectionString); +var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); + +Console.WriteLine("✓ Connected to MongoDB"); + +// Test SQL connection +try +{ + using (var connection = new Microsoft.Data.SqlClient.SqlConnection(sqlConnectionString)) + { + connection.Open(); + Console.WriteLine("✓ Connected to SQL Server\n"); + } +} +catch (Exception ex) +{ + Console.WriteLine($"✗ Failed to connect to SQL Server: {ex.Message}"); + return 1; +} + +Console.WriteLine("Starting data migration...\n"); + +// Migration statistics +int totalCollections = 0; +int totalDocuments = 0; +var errors = new List(); + +try +{ + // Get all collections + var collections = new[] + { + "Pages", "PortfolioCategories", "PortfolioProjects", + "Products", "BlogPosts", "SiteSettings", "MenuItems", + "AdminUsers", "UserRoles", "Orders", "ProductViews", + "HomepageSections" + }; + + foreach (var collectionName in collections) + { + try + { + Console.WriteLine($"Migrating {collectionName}..."); + var collection = mongoDatabase.GetCollection(collectionName); + var documents = await collection.Find(new BsonDocument()).ToListAsync(); + + if (documents.Count == 0) + { + Console.WriteLine($" → 0 documents (skipped)\n"); + continue; + } + + // Export to JSON file for SQL import + var jsonData = new List(); + foreach (var doc in documents) + { + // Convert ObjectId to string + if (doc.Contains("_id")) + { + doc["_id"] = doc["_id"].ToString(); + } + + // Handle nested ObjectIds + ConvertObjectIdsToStrings(doc); + + jsonData.Add(doc.ToJson(new MongoDB.Bson.IO.JsonWriterSettings { OutputMode = MongoDB.Bson.IO.JsonOutputMode.Strict })); + } + + // Save to file + var outputPath = $"/tmp/migration_{collectionName}.json"; + await File.WriteAllTextAsync(outputPath, $"[{string.Join(",", jsonData)}]"); + + Console.WriteLine($" ✓ Exported {documents.Count} documents to {outputPath}"); + totalDocuments += documents.Count; + totalCollections++; + } + catch (Exception ex) + { + errors.Add($"{collectionName}: {ex.Message}"); + Console.WriteLine($" ✗ Error: {ex.Message}"); + } + + Console.WriteLine(); + } + + Console.WriteLine("=============================================="); + Console.WriteLine("Migration Export Complete"); + Console.WriteLine("=============================================="); + Console.WriteLine($"Collections exported: {totalCollections}"); + Console.WriteLine($"Total documents: {totalDocuments}"); + + if (errors.Count > 0) + { + Console.WriteLine($"\nErrors ({errors.Count}):"); + foreach (var error in errors) + { + Console.WriteLine($" - {error}"); + } + } + + Console.WriteLine("\n✓ JSON files created in /tmp/migration_*.json"); + Console.WriteLine("✓ Next step: Run the C# import program to load into SQL Server"); + + return 0; +} +catch (Exception ex) +{ + Console.WriteLine($"\n✗ Fatal error: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + return 1; +} + +void ConvertObjectIdsToStrings(BsonDocument doc) +{ + var keysToConvert = new List(); + + foreach (var element in doc) + { + if (element.Value.IsObjectId) + { + keysToConvert.Add(element.Name); + } + else if (element.Value.IsBsonDocument) + { + ConvertObjectIdsToStrings(element.Value.AsBsonDocument); + } + else if (element.Value.IsBsonArray) + { + foreach (var item in element.Value.AsBsonArray) + { + if (item.IsBsonDocument) + { + ConvertObjectIdsToStrings(item.AsBsonDocument); + } + } + } + } + + foreach (var key in keysToConvert) + { + doc[key] = doc[key].ToString(); + } +} diff --git a/migrate-prepare.sh b/migrate-prepare.sh new file mode 100755 index 0000000..97ee451 --- /dev/null +++ b/migrate-prepare.sh @@ -0,0 +1,164 @@ +#!/bin/bash +# SkyArtShop Migration Preparation Script +# Run this on the SOURCE server + +set -e + +echo "=========================================" +echo " SkyArtShop Migration - Preparation" +echo "=========================================" +echo "" + +# Configuration +BACKUP_DIR=~/skyartshop-backup +DATE=$(date +%Y%m%d_%H%M%S) +APP_DIR=/var/www/SkyArtShop + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Create backup directory +echo -e "${YELLOW}[1/6] Creating backup directory...${NC}" +mkdir -p $BACKUP_DIR/configs +echo -e "${GREEN}✓ Backup directory created: $BACKUP_DIR${NC}" +echo "" + +# Export MongoDB +echo -e "${YELLOW}[2/6] Exporting MongoDB database...${NC}" +if command -v mongodump &> /dev/null; then + mongodump --db=SkyArtShopDB --out=$BACKUP_DIR/mongodb-dump + cd $BACKUP_DIR + tar -czf mongodb-backup-$DATE.tar.gz mongodb-dump/ + rm -rf mongodb-dump/ + + # Get database stats + DB_SIZE=$(du -sh mongodb-backup-$DATE.tar.gz | cut -f1) + echo -e "${GREEN}✓ MongoDB backup created: mongodb-backup-$DATE.tar.gz ($DB_SIZE)${NC}" +else + echo -e "${RED}✗ MongoDB not found! Skipping database backup.${NC}" +fi +echo "" + +# Package application files +echo -e "${YELLOW}[3/6] Packaging application files...${NC}" +if [ -d "$APP_DIR" ]; then + cd /var/www + tar --exclude='SkyArtShop/bin' \ + --exclude='SkyArtShop/obj' \ + --exclude='SkyArtShop/publish/runtimes' \ + --exclude='SkyArtShop/.vs' \ + --exclude='SkyArtShop/.git' \ + --exclude='SkyArtShop/node_modules' \ + -czf $BACKUP_DIR/skyartshop-app-$DATE.tar.gz SkyArtShop/ + + APP_SIZE=$(du -sh $BACKUP_DIR/skyartshop-app-$DATE.tar.gz | cut -f1) + echo -e "${GREEN}✓ Application packaged: skyartshop-app-$DATE.tar.gz ($APP_SIZE)${NC}" +else + echo -e "${RED}✗ Application directory not found: $APP_DIR${NC}" + exit 1 +fi +echo "" + +# Save configuration files +echo -e "${YELLOW}[4/6] Backing up configuration files...${NC}" + +# Nginx config +if [ -f /etc/nginx/sites-available/skyartshop ]; then + cp /etc/nginx/sites-available/skyartshop $BACKUP_DIR/configs/nginx-skyartshop.conf + echo -e "${GREEN}✓ Nginx config backed up${NC}" +else + echo -e "${YELLOW}⚠ No Nginx config found${NC}" +fi + +# Systemd service +if [ -f /etc/systemd/system/skyartshop.service ]; then + cp /etc/systemd/system/skyartshop.service $BACKUP_DIR/configs/skyartshop.service + echo -e "${GREEN}✓ Systemd service backed up${NC}" +else + echo -e "${YELLOW}⚠ No systemd service found${NC}" +fi + +# Save environment info +cat > $BACKUP_DIR/configs/environment-info.txt << EOF +Migration Package Information +============================== +Created: $(date) +Source Server: $(hostname) +IP Address: $(hostname -I | awk '{print $1}') + +.NET Version: +$(dotnet --version 2>/dev/null || echo "Not installed") + +MongoDB Version: +$(mongod --version 2>/dev/null | head -1 || echo "Not installed") + +OS Information: +$(cat /etc/os-release | grep PRETTY_NAME) + +Application Path: $APP_DIR +Backup Date: $DATE +EOF + +echo -e "${GREEN}✓ Environment info saved${NC}" +echo "" + +# Create manifest +echo -e "${YELLOW}[5/6] Creating migration manifest...${NC}" +cat > $BACKUP_DIR/MANIFEST.txt << EOF +======================================== + SkyArtShop Migration Package +======================================== + +Created: $(date) +Source Server: $(hostname) +Package ID: $DATE + +CONTENTS: +--------- +$(ls -lh $BACKUP_DIR/*.tar.gz 2>/dev/null | awk '{print " " $9 " (" $5 ")"}') + +Configuration Files: +$(ls -1 $BACKUP_DIR/configs/ | sed 's/^/ - /') + +Total Package Size: $(du -sh $BACKUP_DIR | cut -f1) + +NEXT STEPS: +----------- +1. Transfer this entire directory to the new server: + scp -r ~/skyartshop-backup username@new-server-ip:/home/username/ + +2. On the new server, run: + cd /var/www/SkyArtShop + bash migrate-restore.sh + +For detailed instructions, see: + MIGRATION_PACKAGE.md + +======================================== +EOF + +echo -e "${GREEN}✓ Manifest created${NC}" +echo "" + +# Display summary +echo -e "${YELLOW}[6/6] Migration package ready!${NC}" +echo "" +echo "=========================================" +echo " 📦 MIGRATION PACKAGE SUMMARY" +echo "=========================================" +cat $BACKUP_DIR/MANIFEST.txt +echo "" +echo -e "${GREEN}✓ All files backed up successfully!${NC}" +echo "" +echo "Next Steps:" +echo "1. Transfer the backup to new server:" +echo -e " ${YELLOW}scp -r $BACKUP_DIR username@new-server-ip:/home/username/${NC}" +echo "" +echo "2. Or create a single archive:" +echo -e " ${YELLOW}cd ~ && tar -czf skyartshop-migration-$DATE.tar.gz skyartshop-backup/${NC}" +echo "" +echo "3. On the new server, extract and run migrate-restore.sh" +echo "" diff --git a/migrate-restore.sh b/migrate-restore.sh new file mode 100755 index 0000000..407da8f --- /dev/null +++ b/migrate-restore.sh @@ -0,0 +1,186 @@ +#!/bin/bash +# SkyArtShop Migration Restoration Script +# Run this on the TARGET server + +set -e + +echo "=========================================" +echo " SkyArtShop Migration - Restoration" +echo "=========================================" +echo "" + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +APP_DIR=/var/www/SkyArtShop +BACKUP_DIR=~/skyartshop-backup + +# Check if backup directory exists +if [ ! -d "$BACKUP_DIR" ]; then + echo -e "${RED}✗ Backup directory not found: $BACKUP_DIR${NC}" + echo "Please transfer the backup files first." + exit 1 +fi + +echo -e "${BLUE}Backup directory found: $BACKUP_DIR${NC}" +echo "" + +# Check for required files +echo -e "${YELLOW}[1/8] Checking backup files...${NC}" +MONGO_BACKUP=$(ls $BACKUP_DIR/mongodb-backup-*.tar.gz 2>/dev/null | head -1) +APP_BACKUP=$(ls $BACKUP_DIR/skyartshop-app-*.tar.gz 2>/dev/null | head -1) + +if [ -z "$MONGO_BACKUP" ]; then + echo -e "${RED}✗ MongoDB backup not found${NC}" + exit 1 +fi + +if [ -z "$APP_BACKUP" ]; then + echo -e "${RED}✗ Application backup not found${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ Found MongoDB backup: $(basename $MONGO_BACKUP)${NC}" +echo -e "${GREEN}✓ Found Application backup: $(basename $APP_BACKUP)${NC}" +echo "" + +# Check prerequisites +echo -e "${YELLOW}[2/8] Checking prerequisites...${NC}" + +if ! command -v dotnet &> /dev/null; then + echo -e "${RED}✗ .NET SDK not installed${NC}" + echo "Install .NET 8.0 first:" + echo " wget https://dot.net/v1/dotnet-install.sh" + echo " bash dotnet-install.sh --channel 8.0" + exit 1 +else + echo -e "${GREEN}✓ .NET SDK found: $(dotnet --version)${NC}" +fi + +if ! command -v mongod &> /dev/null; then + echo -e "${RED}✗ MongoDB not installed${NC}" + echo "Install MongoDB first:" + echo " See MIGRATION_PACKAGE.md for instructions" + exit 1 +else + echo -e "${GREEN}✓ MongoDB found${NC}" +fi + +# Check if MongoDB is running +if ! pgrep -x mongod > /dev/null; then + echo -e "${YELLOW}⚠ MongoDB not running, starting...${NC}" + sudo systemctl start mongod + sleep 3 +fi + +echo -e "${GREEN}✓ MongoDB is running${NC}" +echo "" + +# Restore MongoDB +echo -e "${YELLOW}[3/8] Restoring MongoDB database...${NC}" +cd $BACKUP_DIR +tar -xzf $MONGO_BACKUP +mongorestore --db=SkyArtShopDB mongodb-dump/SkyArtShopDB/ --drop +rm -rf mongodb-dump/ + +# Verify restoration +PRODUCT_COUNT=$(mongosh SkyArtShopDB --quiet --eval "db.Products.countDocuments()" 2>/dev/null || echo "0") +MENU_COUNT=$(mongosh SkyArtShopDB --quiet --eval "db.MenuItems.countDocuments()" 2>/dev/null || echo "0") + +echo -e "${GREEN}✓ MongoDB restored${NC}" +echo -e " - Products: $PRODUCT_COUNT" +echo -e " - Menu Items: $MENU_COUNT" +echo "" + +# Restore application files +echo -e "${YELLOW}[4/8] Restoring application files...${NC}" +sudo mkdir -p /var/www +cd /var/www +sudo tar -xzf $APP_BACKUP + +# Set ownership +sudo chown -R $USER:$USER $APP_DIR + +echo -e "${GREEN}✓ Application files restored to $APP_DIR${NC}" +echo "" + +# Restore NuGet packages +echo -e "${YELLOW}[5/8] Restoring NuGet packages...${NC}" +cd $APP_DIR +dotnet restore +echo -e "${GREEN}✓ NuGet packages restored${NC}" +echo "" + +# Build application +echo -e "${YELLOW}[6/8] Building application...${NC}" +dotnet build -c Release +echo -e "${GREEN}✓ Application built${NC}" +echo "" + +# Publish application +echo -e "${YELLOW}[7/8] Publishing application...${NC}" +dotnet publish SkyArtShop.csproj -c Release -o publish +echo -e "${GREEN}✓ Application published to $APP_DIR/publish${NC}" +echo "" + +# Set permissions +echo -e "${YELLOW}[8/8] Setting permissions...${NC}" +chmod -R 755 $APP_DIR +chmod -R 777 $APP_DIR/wwwroot/uploads +echo -e "${GREEN}✓ Permissions set${NC}" +echo "" + +# Test application +echo -e "${YELLOW}Testing application startup...${NC}" +cd $APP_DIR/publish + +# Kill any existing instance +pkill -f "dotnet.*SkyArtShop.dll" 2>/dev/null || true +sleep 2 + +# Start application +nohup dotnet SkyArtShop.dll --urls "http://0.0.0.0:5001" > /tmp/skyartshop.log 2>&1 & +sleep 5 + +# Test if running +if curl -s http://localhost:5001 > /dev/null; then + echo -e "${GREEN}✓ Application is running on port 5001${NC}" +else + echo -e "${RED}✗ Application failed to start. Check logs:${NC}" + echo " tail -f /tmp/skyartshop.log" +fi +echo "" + +# Display summary +echo "=========================================" +echo " ✅ MIGRATION COMPLETE" +echo "=========================================" +echo "" +echo "Application Details:" +echo " Location: $APP_DIR" +echo " Status: Running on http://localhost:5001" +echo " Database: SkyArtShopDB ($PRODUCT_COUNT products)" +echo "" +echo "Next Steps:" +echo "1. Test the application:" +echo -e " ${YELLOW}curl http://localhost:5001${NC}" +echo "" +echo "2. Configure Nginx (optional):" +echo -e " ${YELLOW}sudo nano /etc/nginx/sites-available/skyartshop${NC}" +echo " (See MIGRATION_PACKAGE.md for config)" +echo "" +echo "3. Create systemd service (optional):" +echo -e " ${YELLOW}sudo nano /etc/systemd/system/skyartshop.service${NC}" +echo " (See MIGRATION_PACKAGE.md for config)" +echo "" +echo "4. Check application logs:" +echo -e " ${YELLOW}tail -f /tmp/skyartshop.log${NC}" +echo "" +echo "Configuration files backed up in:" +echo " $BACKUP_DIR/configs/" +echo "" diff --git a/migrate-transfer.sh b/migrate-transfer.sh new file mode 100755 index 0000000..71de540 --- /dev/null +++ b/migrate-transfer.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# Quick Transfer Script - Use this to send backup to new server + +echo "=========================================" +echo " SkyArtShop - Quick Transfer" +echo "=========================================" +echo "" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Check if backup exists +BACKUP_DIR=~/skyartshop-backup +if [ ! -d "$BACKUP_DIR" ]; then + echo -e "${YELLOW}Backup not found. Running preparation script...${NC}" + bash /var/www/SkyArtShop/migrate-prepare.sh +fi + +# Get new server details +echo -e "${BLUE}Enter new server details:${NC}" +read -p "Username: " NEW_USER +read -p "IP Address: " NEW_IP +echo "" + +# Create single archive +echo -e "${YELLOW}Creating single archive for easy transfer...${NC}" +DATE=$(date +%Y%m%d_%H%M%S) +cd ~ +tar -czf skyartshop-complete-$DATE.tar.gz skyartshop-backup/ +ARCHIVE_SIZE=$(du -sh skyartshop-complete-$DATE.tar.gz | cut -f1) +echo -e "${GREEN}✓ Archive created: skyartshop-complete-$DATE.tar.gz ($ARCHIVE_SIZE)${NC}" +echo "" + +# Test connection +echo -e "${YELLOW}Testing connection to new server...${NC}" +if ssh -o ConnectTimeout=5 $NEW_USER@$NEW_IP "echo 'Connection successful'" 2>/dev/null; then + echo -e "${GREEN}✓ Connection successful${NC}" + echo "" + + # Transfer file + echo -e "${YELLOW}Transferring archive to new server...${NC}" + echo "This may take several minutes depending on file size and network speed..." + + scp ~/skyartshop-complete-$DATE.tar.gz $NEW_USER@$NEW_IP:/tmp/ + + if [ $? -eq 0 ]; then + echo -e "${GREEN}✓ Transfer complete!${NC}" + echo "" + + # Extract on remote server + echo -e "${YELLOW}Extracting on new server...${NC}" + ssh $NEW_USER@$NEW_IP "cd ~ && tar -xzf /tmp/skyartshop-complete-$DATE.tar.gz && rm /tmp/skyartshop-complete-$DATE.tar.gz" + + echo -e "${GREEN}✓ Files extracted on new server${NC}" + echo "" + + # Display next steps + echo "=========================================" + echo " ✅ TRANSFER COMPLETE" + echo "=========================================" + echo "" + echo "Next Steps - Run on NEW SERVER:" + echo "" + echo "1. SSH to the new server:" + echo -e " ${YELLOW}ssh $NEW_USER@$NEW_IP${NC}" + echo "" + echo "2. Install prerequisites (if not already installed):" + echo -e " ${YELLOW}bash ~/skyartshop-backup/install-prerequisites.sh${NC}" + echo "" + echo "3. Run restoration script:" + echo -e " ${YELLOW}bash ~/skyartshop-backup/migrate-restore.sh${NC}" + echo "" + echo "4. Follow the on-screen instructions" + echo "" + + else + echo -e "${RED}✗ Transfer failed${NC}" + echo "Try manual transfer:" + echo -e " ${YELLOW}scp ~/skyartshop-complete-$DATE.tar.gz $NEW_USER@$NEW_IP:/tmp/${NC}" + fi + +else + echo -e "${RED}✗ Cannot connect to new server${NC}" + echo "" + echo "Manual transfer options:" + echo "" + echo "1. Using SCP:" + echo -e " ${YELLOW}scp ~/skyartshop-complete-$DATE.tar.gz $NEW_USER@$NEW_IP:/tmp/${NC}" + echo "" + echo "2. Using USB drive:" + echo -e " ${YELLOW}cp ~/skyartshop-complete-$DATE.tar.gz /media/usb-drive/${NC}" + echo "" + echo "3. Using cloud storage:" + echo " Upload ~/skyartshop-complete-$DATE.tar.gz to your cloud provider" + echo "" +fi diff --git a/nginx-skyartshop-secured.conf b/nginx-skyartshop-secured.conf new file mode 100755 index 0000000..ac115d4 --- /dev/null +++ b/nginx-skyartshop-secured.conf @@ -0,0 +1,159 @@ +# Upstream configuration +upstream skyartshop_backend { + server localhost:5000; + keepalive 64; +} + +# Rate limiting zones - Enhanced for security +limit_req_zone $binary_remote_addr zone=general:10m rate=100r/s; +limit_req_zone $binary_remote_addr zone=admin:10m rate=10r/s; # Reduced from 20 to 10 +limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; # Only 5 login attempts per minute +limit_conn_zone $binary_remote_addr zone=addr:10m; + +# Block common attack patterns +map $request_uri $is_blocked { + default 0; + "~*\.(asp|aspx|php|jsp|cgi)$" 1; + # Block common SQL injection keywords except allow legitimate admin endpoints + "~*(eval|base64|decode|union|select|insert|update|drop)" 1; + "~*(/\.|\.\./)" 1; +} + +server { + server_name skyarts.ddns.net localhost; + + # Block malicious requests + if ($is_blocked = 1) { + return 444; + } + + # Enhanced Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + + # Content Security Policy - Allow only trusted CDNs + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://cdn.ckeditor.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; img-src 'self' data: https:; connect-src 'self' https://cdn.jsdelivr.net https://cdn.ckeditor.com; frame-src 'none';" always; + + # Prevent clickjacking + add_header X-Permitted-Cross-Domain-Policies "none" always; + + # HSTS - Force HTTPS + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + + # Connection limits + limit_conn addr 10; + + # General rate limiting + limit_req zone=general burst=200 nodelay; + + # Logging + access_log /var/log/nginx/skyartshop_access.log combined buffer=32k; + error_log /var/log/nginx/skyartshop_error.log warn; + + # Block access to sensitive files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + # Block access to config files + location ~* \.(conf|config|json|lock|git|env)$ { + deny all; + } + + # Admin login with strict rate limiting + location = /admin/login { + limit_req zone=login burst=2 nodelay; + limit_req zone=admin burst=5 nodelay; + + proxy_pass http://skyartshop_backend; + 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; + proxy_set_header X-Real-IP $remote_addr; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + client_max_body_size 1M; + } + + # Admin area with stricter rate limiting + location /admin { + limit_req zone=admin burst=10 nodelay; + + proxy_pass http://skyartshop_backend; + 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; + proxy_set_header X-Real-IP $remote_addr; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + client_max_body_size 50M; + } + + # Main application - catch all + location / { + proxy_pass http://skyartshop_backend; + proxy_http_version 1.1; + + # Connection headers + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection keep-alive; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + + # Forwarding headers + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # Buffer settings for performance + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 24 4k; + proxy_busy_buffers_size 8k; + proxy_max_temp_file_size 2048m; + + client_max_body_size 50M; + } + + listen [::]:443 ssl ipv6only=on; # managed by Certbot + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/skyarts.ddns.net/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/skyarts.ddns.net/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot +} + +server { + if ($host = skyarts.ddns.net) { + return 301 https://$host$request_uri; + } # managed by Certbot + + listen 80; + listen [::]:80; + server_name skyarts.ddns.net localhost; + return 404; # managed by Certbot +} diff --git a/quick-restart.sh b/quick-restart.sh new file mode 100755 index 0000000..50e6283 --- /dev/null +++ b/quick-restart.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Quick restart without rebuild (for configuration changes) + +echo "Restarting SkyArtShop services..." + +sudo systemctl restart skyartshop +sleep 2 + +if systemctl is-active --quiet skyartshop; then + echo "✓ Application restarted successfully" + + # Quick test + if curl -s -o /dev/null -w "%{http_code}" http://localhost | grep -q "200"; then + echo "✓ Website is responding" + else + echo "⚠ Website may have issues" + fi +else + echo "✗ Application failed to start" + echo "View logs: sudo journalctl -u skyartshop.service -n 50" +fi diff --git a/quick-update.ps1 b/quick-update.ps1 new file mode 100755 index 0000000..2f9aec0 --- /dev/null +++ b/quick-update.ps1 @@ -0,0 +1,57 @@ +# Quick View Update - Copy changed view files without restarting site +# Run this after editing views to update live site without downtime + +param( + [string]$ViewFile = "" +) + +$devPath = "E:\Documents\Website Projects\Sky_Art_Shop" +$prodPath = "C:\inetpub\wwwroot\skyartshop" + +if ($ViewFile) { + # Copy specific file + $sourceFile = Join-Path $devPath $ViewFile + $destFile = Join-Path $prodPath $ViewFile + + if (Test-Path $sourceFile) { + $destDir = Split-Path $destFile -Parent + if (-not (Test-Path $destDir)) { + New-Item -ItemType Directory -Path $destDir -Force | Out-Null + } + Copy-Item $sourceFile $destFile -Force + Write-Host "✅ Updated: $ViewFile" -ForegroundColor Green + + # Touch web.config to trigger reload without restart + $webConfig = Join-Path $prodPath "web.config" + if (Test-Path $webConfig) { + (Get-Item $webConfig).LastWriteTime = Get-Date + Write-Host "✅ Site reloaded automatically (no downtime)" -ForegroundColor Cyan + } + } + else { + Write-Host "❌ File not found: $sourceFile" -ForegroundColor Red + } +} +else { + # Sync all Views + Write-Host "🔄 Syncing all views..." -ForegroundColor Cyan + + $viewsSource = Join-Path $devPath "Views" + $viewsDest = Join-Path $prodPath "Views" + + if (Test-Path $viewsSource) { + Copy-Item -Path $viewsSource -Destination $prodPath -Recurse -Force + Write-Host "✅ All views synced" -ForegroundColor Green + + # Touch web.config + $webConfig = Join-Path $prodPath "web.config" + if (Test-Path $webConfig) { + (Get-Item $webConfig).LastWriteTime = Get-Date + Write-Host "✅ Site reloaded automatically" -ForegroundColor Cyan + } + } +} + +Write-Host "`n💡 Usage examples:" -ForegroundColor Yellow +Write-Host " .\quick-update.ps1 # Sync all views" +Write-Host " .\quick-update.ps1 'Views\Shared\_AdminLayout.cshtml' # Update specific view" diff --git a/rebuild-and-deploy.sh b/rebuild-and-deploy.sh new file mode 100755 index 0000000..f91c25f --- /dev/null +++ b/rebuild-and-deploy.sh @@ -0,0 +1,119 @@ +#!/bin/bash + +# SkyArtShop - Rebuild and Deploy Script +# Run this script after making changes to your code + +set -e # Exit on error + +echo "========================================" +echo " SkyArtShop - Rebuild & Deploy" +echo "========================================" +echo "" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +# Change to project directory +cd /var/www/SkyArtShop + +# Step 1: Build +echo -e "${YELLOW}[1/5] Building application...${NC}" +dotnet build SkyArtShop.csproj -c Release +if [ $? -eq 0 ]; then + echo -e "${GREEN}✓ Build successful${NC}" +else + echo -e "${RED}✗ Build failed${NC}" + exit 1 +fi +echo "" + +# Step 2: Clean and Publish +echo -e "${YELLOW}[2/5] Cleaning old publish directory...${NC}" +rm -rf ./publish_temp +mkdir -p ./publish_temp + +echo -e "${YELLOW}[2/5] Publishing to ./publish_temp...${NC}" +dotnet publish SkyArtShop.csproj -c Release -o ./publish_temp --no-build +if [ $? -eq 0 ]; then + echo -e "${GREEN}✓ Publish successful${NC}" +else + echo -e "${RED}✗ Publish failed${NC}" + exit 1 +fi +echo "" + +# Step 3: Stop service and swap directories +echo -e "${YELLOW}[3/5] Stopping application...${NC}" +sudo systemctl stop skyartshop +sleep 1 + +echo -e "${YELLOW}[3/5] Swapping publish directories...${NC}" +if [ -d "./publish" ]; then + sudo rm -rf ./publish_old + sudo mv ./publish ./publish_old +fi +mv ./publish_temp ./publish + +echo -e "${YELLOW}[3/5] Setting permissions...${NC}" +sudo chown -R www-data:www-data ./publish +sudo chmod +x ./publish/SkyArtShop.dll +echo -e "${GREEN}✓ Permissions set${NC}" +echo "" + +# Step 4: Restart services +echo -e "${YELLOW}[4/5] Restarting services...${NC}" +sudo systemctl restart skyartshop +echo " Waiting for application to start..." +sleep 5 + +if systemctl is-active --quiet skyartshop; then + echo -e "${GREEN}✓ Application restarted${NC}" +else + echo -e "${RED}✗ Application failed to start${NC}" + echo "Check logs: sudo journalctl -u skyartshop.service -n 50" + exit 1 +fi +echo "" + +# Step 5: Verify +echo -e "${YELLOW}[5/5] Verifying deployment...${NC}" +echo " Waiting for application to be ready..." +sleep 3 + +# Test website +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost 2>/dev/null) +if [ "$HTTP_CODE" = "200" ]; then + echo -e "${GREEN}✓ Website responding (HTTP $HTTP_CODE)${NC}" +else + echo -e "${RED}✗ Website not responding (HTTP $HTTP_CODE)${NC}" + exit 1 +fi + +# Test CSS +CSS_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/assets/css/main.css 2>/dev/null) +if [ "$CSS_CODE" = "200" ]; then + echo -e "${GREEN}✓ CSS loading (HTTP $CSS_CODE)${NC}" +else + echo -e "${RED}✗ CSS not loading (HTTP $CSS_CODE)${NC}" +fi + +# Test JS +JS_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/assets/js/main.js 2>/dev/null) +if [ "$JS_CODE" = "200" ]; then + echo -e "${GREEN}✓ JavaScript loading (HTTP $JS_CODE)${NC}" +else + echo -e "${RED}✗ JavaScript not loading (HTTP $JS_CODE)${NC}" +fi + +echo "" +echo "========================================" +echo -e "${GREEN}✓ Deployment Complete!${NC}" +echo "========================================" +echo "" +echo "Website: http://localhost" +echo "Domain: http://skyarts.ddns.net" +echo "" +echo "To view logs: sudo journalctl -u skyartshop.service -f" diff --git a/reset-admin-password-mongo.sh b/reset-admin-password-mongo.sh new file mode 100755 index 0000000..8fd5197 --- /dev/null +++ b/reset-admin-password-mongo.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# MongoDB-Based Admin Password Reset Script +# No more SQLite issues! + +if [ -z "$1" ]; then + echo "Usage: $0 " + echo "Example: $0 MyNewPassword123!" + exit 1 +fi + +NEW_PASSWORD="$1" +EMAIL="admin@skyartshop.com" + +echo "Resetting admin password in MongoDB..." + +# Stop the service +echo "Stopping service..." +sudo systemctl stop skyartshop.service + +# Update the password in appsettings.Production.json +echo "Updating password in configuration..." +cd /var/www/SkyArtShop +sudo sed -i "s|\"Password\": \".*\"|\"Password\": \"$NEW_PASSWORD\"|g" publish/appsettings.Production.json + +# Delete the admin user from MongoDB to force recreation +echo "Removing old admin user from MongoDB..." +mongosh SkyArtShopDB --eval "db.AdminUsers.deleteMany({Email: '$EMAIL'})" --quiet + +# Start the service (it will recreate the admin with new password) +echo "Starting service..." +sudo systemctl start skyartshop.service + +sleep 3 + +# Check status +systemctl status skyartshop.service --no-pager | head -15 + +echo "" +echo "✅ Admin password has been reset in MongoDB!" +echo "Email: $EMAIL" +echo "Password: $NEW_PASSWORD" +echo "" +echo "You can now login at: https://skyarts.ddns.net/admin/login" diff --git a/reset-admin-password.sh b/reset-admin-password.sh new file mode 100755 index 0000000..eebcdc0 --- /dev/null +++ b/reset-admin-password.sh @@ -0,0 +1,78 @@ +#!/bin/bash + +# SkyArt Shop - Admin Password Reset Tool +# Usage: ./reset-admin-password.sh [new_password] + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +RESET_SCRIPT="$SCRIPT_DIR/reset-password.csx" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo "" +echo "==========================================" +echo " SkyArt Shop - Password Reset Tool" +echo "==========================================" +echo "" + +# Check if dotnet-script is installed +if ! command -v dotnet-script &> /dev/null; then + echo -e "${RED}✗ Error: dotnet-script is not installed${NC}" + echo "" + echo "To install, run:" + echo " /home/pts/.dotnet/dotnet tool install -g dotnet-script" + echo " export PATH=\"\$PATH:/home/pts/.dotnet/tools\"" + echo "" + exit 1 +fi + +# Check if reset script exists +if [ ! -f "$RESET_SCRIPT" ]; then + echo -e "${RED}✗ Error: Reset script not found at $RESET_SCRIPT${NC}" + exit 1 +fi + +# Get password from argument or prompt +if [ -n "$1" ]; then + NEW_PASSWORD="$1" +else + echo -e "${YELLOW}Enter new password (or press Enter for default 'Admin123!'):${NC}" + read -s NEW_PASSWORD + echo "" + + if [ -z "$NEW_PASSWORD" ]; then + NEW_PASSWORD="Admin123!" + echo "Using default password: Admin123!" + fi +fi + +echo "" +echo "Resetting password for: admin@skyartshop.com" +echo "" + +# Add dotnet-script to PATH if needed +export PATH="$PATH:/home/pts/.dotnet/tools" + +# Run the reset script +if dotnet-script "$RESET_SCRIPT" "$NEW_PASSWORD"; then + echo "" + echo -e "${GREEN}==========================================" + echo "✓ Password reset complete!" + echo "==========================================${NC}" + echo "" + echo "You can now login at:" + echo " https://skyarts.ddns.net/admin/login" + echo "" + echo "With credentials:" + echo " Email: admin@skyartshop.com" + echo " Password: $NEW_PASSWORD" + echo "" +else + echo "" + echo -e "${RED}✗ Password reset failed${NC}" + echo "" + exit 1 +fi diff --git a/reset-admin-simple.sh b/reset-admin-simple.sh new file mode 100755 index 0000000..9504871 --- /dev/null +++ b/reset-admin-simple.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +# Simple Admin Password Reset Script +# This creates a new admin with a simple known password + +ADMIN_EMAIL="admin@skyartshop.com" +NEW_PASSWORD="Admin123!" + +echo "==========================================" +echo "SkyArt Shop - Admin Password Reset" +echo "==========================================" +echo "" +echo "Resetting password for: $ADMIN_EMAIL" +echo "New password will be: $NEW_PASSWORD" +echo "" + +# Delete existing admin +mongosh --quiet SkyArtShopDB < /tmp/hash_password.cs << 'CSHARP' +using System; +using System.Security.Cryptography; + +var password = args.Length > 0 ? args[0] : "Admin123!"; + +using var rng = RandomNumberGenerator.Create(); +var salt = new byte[16]; +rng.GetBytes(salt); + +using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA256); +var hash = pbkdf2.GetBytes(32); + +var hashBytes = new byte[48]; +Array.Copy(salt, 0, hashBytes, 0, 16); +Array.Copy(hash, 0, hashBytes, 16, 32); + +Console.WriteLine(Convert.ToBase64String(hashBytes)); +CSHARP + +# Generate the hash +HASH=$(dotnet script /tmp/hash_password.cs "$NEW_PASSWORD" 2>/dev/null) + +if [ -z "$HASH" ]; then + echo "✗ Failed to generate password hash" + exit 1 +fi + +echo "Generated hash: ${HASH:0:40}..." +echo "" + +# Update MongoDB +mongosh --quiet SkyArtShopDB < 0) { + print('✓ Password updated successfully'); +} else { + print('✗ Failed to update password'); +} +EOF + +echo "" +echo "==========================================" +echo "✓ Password Reset Complete!" +echo "==========================================" +echo "" +echo "Login credentials:" +echo " Email: $ADMIN_EMAIL" +echo " Password: $NEW_PASSWORD" +echo "" +echo "Test login:" +echo " https://skyarts.ddns.net/admin/login" +echo "" + +# Clean up +rm -f /tmp/hash_password.cs diff --git a/reset-password.csx b/reset-password.csx new file mode 100755 index 0000000..0eb8e04 --- /dev/null +++ b/reset-password.csx @@ -0,0 +1,118 @@ +#!/usr/bin/env dotnet-script +#r "nuget: MongoDB.Driver, 2.23.1" + +using System; +using System.Security.Cryptography; +using MongoDB.Driver; +using MongoDB.Bson; + +// Configuration +var connectionString = "mongodb://localhost:27017"; +var databaseName = "SkyArtShopDB"; +var collectionName = "AdminUsers"; +var adminEmail = "admin@skyartshop.com"; + +// Get password from command line or use default +var newPassword = Args.Count > 0 ? Args[0] : "Admin123!"; + +Console.WriteLine("========================================"); +Console.WriteLine("SkyArt Shop - Direct Password Reset"); +Console.WriteLine("========================================"); +Console.WriteLine(); +Console.WriteLine($"Email: {adminEmail}"); +Console.WriteLine($"New Password: {newPassword}"); +Console.WriteLine(); + +// Hash password using PBKDF2 (same as AuthService) +string HashPassword(string password) +{ + using var rng = RandomNumberGenerator.Create(); + var salt = new byte[16]; + rng.GetBytes(salt); + + using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA256); + var hash = pbkdf2.GetBytes(32); + + var hashBytes = new byte[48]; + Array.Copy(salt, 0, hashBytes, 0, 16); + Array.Copy(hash, 0, hashBytes, 16, 32); + + return Convert.ToBase64String(hashBytes); +} + +try +{ + var client = new MongoClient(connectionString); + var database = client.GetDatabase(databaseName); + var collection = database.GetCollection(collectionName); + + // Find existing admin + var filter = Builders.Filter.Eq("Email", adminEmail); + var existingUser = await collection.Find(filter).FirstOrDefaultAsync(); + + var newPasswordHash = HashPassword(newPassword); + + if (existingUser != null) + { + // Update existing user + var update = Builders.Update + .Set("PasswordHash", newPasswordHash) + .Set("LastLogin", DateTime.UtcNow); + + var result = await collection.UpdateOneAsync(filter, update); + + if (result.ModifiedCount > 0) + { + Console.WriteLine("✓ Password updated successfully!"); + } + else + { + Console.WriteLine("⚠️ User found but not updated"); + } + } + else + { + // Create new admin user + var newUser = new BsonDocument + { + { "Email", adminEmail }, + { "PasswordHash", newPasswordHash }, + { "Name", "System Administrator" }, + { "Role", "MasterAdmin" }, + { "Permissions", new BsonArray + { + "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 }, + { "CreatedAt", DateTime.UtcNow }, + { "LastLogin", BsonNull.Value }, + { "CreatedBy", "Direct Reset Script" }, + { "Phone", BsonNull.Value }, + { "Notes", $"Created via direct reset on {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}" } + }; + + await collection.InsertOneAsync(newUser); + Console.WriteLine("✓ New admin user created!"); + } + + Console.WriteLine(); + Console.WriteLine("========================================"); + Console.WriteLine("✓ Success!"); + Console.WriteLine("========================================"); + Console.WriteLine(); + Console.WriteLine("Login credentials:"); + Console.WriteLine($" Email: {adminEmail}"); + Console.WriteLine($" Password: {newPassword}"); + Console.WriteLine(); + Console.WriteLine("Login URL: https://skyarts.ddns.net/admin/login"); + Console.WriteLine(); +} +catch (Exception ex) +{ + Console.WriteLine($"✗ Error: {ex.Message}"); + Environment.Exit(1); +} diff --git a/setup-database.sh b/setup-database.sh new file mode 100755 index 0000000..a7339d8 --- /dev/null +++ b/setup-database.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# SQL Server Database Setup Script +# Creates the SkyArtShop database and required tables + +echo "==================================" +echo "SkyArtShop Database Setup" +echo "==================================" + +# Prompt for SA password +read -sp "Enter SA password: " SA_PASSWORD +echo "" + +# Database name +DB_NAME="SkyArtShopDB" + +# Create database +echo "Creating database $DB_NAME..." + +/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -Q " +IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = N'$DB_NAME') +BEGIN + CREATE DATABASE $DB_NAME; + PRINT 'Database $DB_NAME created successfully.'; +END +ELSE +BEGIN + PRINT 'Database $DB_NAME already exists.'; +END +" + +echo "" +echo "Database setup complete!" +echo "" +echo "Connection String for appsettings.json:" +echo "Server=localhost,1433;Database=$DB_NAME;User Id=sa;Password=YOUR_PASSWORD;TrustServerCertificate=True;MultipleActiveResultSets=True" +echo "" diff --git a/sync-images.sh b/sync-images.sh new file mode 100755 index 0000000..e4c725d --- /dev/null +++ b/sync-images.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Image Sync Script for SkyArtShop +# Keeps uploads directory in sync between main and publish folders + +echo "===================================" +echo "SkyArtShop Image Sync Script" +echo "===================================" + +SOURCE_DIR="/var/www/SkyArtShop/wwwroot/uploads" +PUBLISH_DIR="/var/www/SkyArtShop/publish/wwwroot/uploads" + +# Ensure directories exist +mkdir -p "$SOURCE_DIR/images" +mkdir -p "$PUBLISH_DIR/images" + +echo "" +echo "Source: $SOURCE_DIR" +echo "Publish: $PUBLISH_DIR" +echo "" + +# Sync from source to publish (main direction) +echo "Syncing from main wwwroot to publish..." +rsync -av --progress "$SOURCE_DIR/" "$PUBLISH_DIR/" + +echo "" +echo "Checking for files in publish not in source..." +# Also sync back any files that might be in publish but not in source +rsync -av --ignore-existing "$PUBLISH_DIR/" "$SOURCE_DIR/" + +echo "" +echo "===================================" +echo "Image Sync Complete!" +echo "===================================" + +# Show statistics +SOURCE_COUNT=$(find "$SOURCE_DIR/images" -type f | wc -l) +PUBLISH_COUNT=$(find "$PUBLISH_DIR/images" -type f | wc -l) + +echo "" +echo "Files in source: $SOURCE_COUNT" +echo "Files in publish: $PUBLISH_COUNT" +echo "" + +# Set proper permissions +echo "Setting permissions..." +chmod -R 755 "$SOURCE_DIR" +chmod -R 755 "$PUBLISH_DIR" + +echo "Done!" diff --git a/sync-uploads.ps1 b/sync-uploads.ps1 new file mode 100755 index 0000000..93cf929 --- /dev/null +++ b/sync-uploads.ps1 @@ -0,0 +1,29 @@ +# Sync uploaded images between live site and localhost +# Run this script to sync images uploaded on the live site to your development environment + +$liveUploads = "C:\inetpub\wwwroot\skyartshop\wwwroot\uploads\images" +$devUploads = "E:\Documents\Website Projects\Sky_Art_Shop\wwwroot\uploads\images" + +Write-Host "🔄 Syncing uploads from live site to localhost..." -ForegroundColor Cyan + +# Copy new files from live to dev +Get-ChildItem -Path $liveUploads -File | ForEach-Object { + $devFile = Join-Path $devUploads $_.Name + if (-not (Test-Path $devFile) -or $_.LastWriteTime -gt (Get-Item $devFile).LastWriteTime) { + Copy-Item $_.FullName $devFile -Force + Write-Host " ✅ Copied: $($_.Name)" -ForegroundColor Green + } +} + +# Copy new files from dev to live (if you upload locally) +Get-ChildItem -Path $devUploads -File | ForEach-Object { + $liveFile = Join-Path $liveUploads $_.Name + if (-not (Test-Path $liveFile) -or $_.LastWriteTime -gt (Get-Item $liveFile).LastWriteTime) { + Copy-Item $_.FullName $liveFile -Force + Write-Host " ✅ Copied to live: $($_.Name)" -ForegroundColor Yellow + } +} + +Write-Host "`n✨ Sync complete!" -ForegroundColor Green +Write-Host "📊 Total images in dev: $((Get-ChildItem $devUploads).Count)" -ForegroundColor Cyan +Write-Host "📊 Total images in live: $((Get-ChildItem $liveUploads).Count)" -ForegroundColor Cyan diff --git a/test-homepage-editor.sh b/test-homepage-editor.sh new file mode 100755 index 0000000..45348ad --- /dev/null +++ b/test-homepage-editor.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Test Homepage Editor Functionality + +echo "==========================================" +echo "Testing Homepage Editor" +echo "==========================================" +echo "" + +# Check if user can access homepage editor +echo "1. Testing Homepage Editor Access..." +curl -s -c /tmp/homepage_test.txt -X POST http://localhost:5000/admin/login \ + -d "email=admin@skyartshop.com&password=Admin123!" -L > /dev/null 2>&1 + +curl -s -b /tmp/homepage_test.txt http://localhost:5000/admin/homepage | grep -q "Homepage Editor" && \ + echo " ✓ Homepage Editor accessible" || echo " ✗ Homepage Editor not accessible" + +echo "" +echo "2. Checking Database Structure..." +PGPASSWORD='SkyArt2025Pass!' psql -h localhost -U skyartapp -d skyartshop -t -c \ + "SELECT column_name FROM information_schema.columns WHERE table_name = 'homepagesections' ORDER BY ordinal_position;" | \ + tr '\n' ', ' && echo "" + +echo "" +echo "3. Current Homepage Sections:" +PGPASSWORD='SkyArt2025Pass!' psql -h localhost -U skyartapp -d skyartshop -c \ + "SELECT id, sectiontype, title, isactive, displayorder FROM homepagesections ORDER BY displayorder;" + +echo "" +echo "4. Required Fields for Create:" +echo " ✓ SectionType (required) - hero, inspiration, collection, promotion, custom" +echo " ✓ Title (required)" +echo " - Subtitle (optional)" +echo " - Content (optional, uses CKEditor)" +echo " - ImageUrl (optional, file upload)" +echo " - ButtonText (optional)" +echo " - ButtonUrl (optional)" +echo " ✓ IsActive (checkbox, defaults to true)" +echo " ✓ DisplayOrder (auto-calculated)" +echo " ✓ AdditionalData (auto-initialized as empty dictionary)" + +echo "" +echo "5. Access URLs:" +echo " Admin Editor: https://skyarts.ddns.net/admin/homepage" +echo " Create Section: https://skyarts.ddns.net/admin/homepage/section/create" +echo " Frontend: https://skyarts.ddns.net/" + +echo "" +echo "6. Testing Section Creation (SQL INSERT)..." +PGPASSWORD='SkyArt2025Pass!' psql -h localhost -U skyartapp -d skyartshop << 'EOF' +INSERT INTO homepagesections ( + id, sectiontype, title, subtitle, content, imageurl, + buttontext, buttonurl, isactive, displayorder, + additionaldata, createdat, updatedat +) VALUES ( + gen_random_uuid()::text, + 'hero', + 'Welcome to Sky Art Shop', + 'Scrapbooking and Journaling Fun', + '

Explore the world of creativity and self-expression

', + '/uploads/images/hero-banner.jpg', + 'Shop Now', + '/shop', + true, + 0, + '{}'::jsonb, + now(), + now() +) ON CONFLICT (id) DO NOTHING; +EOF + +echo "" +echo "7. Verifying Section Created:" +PGPASSWORD='SkyArt2025Pass!' psql -h localhost -U skyartapp -d skyartshop -c \ + "SELECT id, title, sectiontype, isactive FROM homepagesections WHERE sectiontype = 'hero';" + +echo "" +echo "==========================================" +echo "✅ Homepage Editor Ready!" +echo "==========================================" +echo "" +echo "Next Steps:" +echo "1. Visit: https://skyarts.ddns.net/admin/homepage" +echo "2. Click 'Create New Section'" +echo "3. Fill in required fields (Section Type, Title)" +echo "4. Click 'Create Section'" +echo "5. View on homepage: https://skyarts.ddns.net/" diff --git a/test-image-picker.html b/test-image-picker.html new file mode 100755 index 0000000..68aa295 --- /dev/null +++ b/test-image-picker.html @@ -0,0 +1,57 @@ + + + + + + Image Picker Test + + +

Image Picker Debugging Instructions

+ +

Test Steps:

+
    +
  1. Go to: Homepage Editor
  2. +
  3. Click on any existing section's "Edit" button
  4. +
  5. Scroll down to "Section Image"
  6. +
  7. Click "Select/Upload Image" button
  8. +
  9. Open Browser Developer Console (F12)
  10. +
  11. Watch for console logs that will show: +
      +
    • "Opening image picker. Mode: single Callback: function"
    • +
    • "Modal opened successfully"
    • +
    • When you click an image: "Toggle selection for: [URL] Mode: single"
    • +
    • When you click "Select Images": "Confirm selection clicked. Selected images: [array]"
    • +
    • After callback: "Image selection successful!"
    • +
    +
  12. +
+ +

What Was Fixed:

+
    +
  • ✅ Fixed ID attribute on hidden ImageUrl field
  • +
  • ✅ Fixed case mismatch in SelectedImageUrl field
  • +
  • ✅ Changed onclick inline handlers to event listeners (prevents URL escaping issues)
  • +
  • ✅ Added comprehensive console logging for debugging
  • +
  • ✅ Added error handling with try-catch blocks
  • +
  • ✅ Added validation checks for all DOM elements
  • +
  • ✅ Added user-friendly error alerts
  • +
+ +

Expected Behavior:

+

When you select an image and click "Select Images (1)":

+
    +
  • The modal should close
  • +
  • The image preview should appear below the button
  • +
  • The SelectedImageUrl hidden field should contain the image URL
  • +
  • When you submit the form, the image should be saved to the section
  • +
+ +

If Issues Persist:

+

Check the browser console for error messages. Common issues:

+
    +
  • Red error about "callback function not found" - refresh the page
  • +
  • No images loading - check /admin/upload/list endpoint
  • +
  • Modal not opening - Bootstrap JS not loaded properly
  • +
+ + diff --git a/test-live-dashboard.sh b/test-live-dashboard.sh new file mode 100755 index 0000000..91595da --- /dev/null +++ b/test-live-dashboard.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Test Live Dashboard System Status + +echo "==========================================" +echo "Testing Live Dashboard System Status" +echo "==========================================" +echo "" + +# Test endpoint accessibility +echo "1. Testing system-status endpoint..." +STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/admin/system-status) +if [ "$STATUS_CODE" = "302" ] || [ "$STATUS_CODE" = "401" ]; then + echo " ✓ Endpoint exists (redirecting to login - expected)" +else + echo " Status code: $STATUS_CODE" +fi + +echo "" +echo "2. Dashboard Features:" +echo " ✓ Live server time (updates every second)" +echo " ✓ PostgreSQL connection status (checks every 10 seconds)" +echo " ✓ Database information (host, name, version)" +echo " ✓ System online indicator" +echo " ✓ Admin user display" +echo " ✓ Site name display" + +echo "" +echo "3. System Status Response Format:" +echo " { + \"databaseConnected\": true, + \"dbType\": \"PostgreSQL\", + \"dbHost\": \"localhost\", + \"dbName\": \"skyartshop\", + \"dbVersion\": \"16\", + \"userCount\": 1, + \"timestamp\": \"2025-12-06 14:20:00 UTC\" + }" + +echo "" +echo "4. Accessing Dashboard:" +echo " URL: http://SERVER_IP:5000/admin/dashboard" +echo " Login: admin@skyartshop.com / Admin123!" +echo "" +echo " Once logged in, you will see:" +echo " - Live clock updating in real-time" +echo " - Green PostgreSQL Connected badge" +echo " - Database info: localhost | skyartshop" +echo " - System: Online (green badge)" +echo "" +echo "==========================================" +echo "✅ Live dashboard system is ready!" +echo "==========================================" diff --git a/test-product-save.sh b/test-product-save.sh new file mode 100755 index 0000000..984868f --- /dev/null +++ b/test-product-save.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Test script to verify product save functionality + +echo "Testing Product Save Functionality" +echo "====================================" +echo "" + +# Test 1: Check if create page loads +echo "Test 1: Checking if product create page loads..." +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/admin/products/create) +if [ "$HTTP_CODE" == "200" ] || [ "$HTTP_CODE" == "302" ]; then + echo "✅ Create page accessible (HTTP $HTTP_CODE)" +else + echo "❌ Create page failed (HTTP $HTTP_CODE)" +fi +echo "" + +# Test 2: Check if form has antiforgery token +echo "Test 2: Checking if form includes antiforgery token..." +if curl -s http://localhost:5000/admin/products/create | grep -q "AntiForgeryToken\|__RequestVerificationToken"; then + echo "✅ Antiforgery token found in form" +else + echo "⚠️ Antiforgery token not found (may require authentication)" +fi +echo "" + +# Test 3: Monitor logs for errors +echo "Test 3: Monitoring application logs for recent errors..." +echo "Recent application logs:" +journalctl -u skyartshop.service -n 10 --no-pager --since "1 minute ago" | grep -i "error\|exception" || echo "✅ No errors in recent logs" +echo "" + +echo "====================================" +echo "Test complete!" +echo "" +echo "To test product save manually:" +echo "1. Navigate to https://skyarts.ddns.net/admin/products/create" +echo "2. Fill in product details (Name and Price are required)" +echo "3. Click 'Save Product'" +echo "4. Check logs with: journalctl -u skyartshop.service -f" diff --git a/test-user-management.sh b/test-user-management.sh new file mode 100755 index 0000000..1440e98 --- /dev/null +++ b/test-user-management.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# Test User Management System +# This script tests the complete user management functionality + +BASE_URL="http://localhost:5000" +COOKIE_FILE="/tmp/admin_cookies.txt" + +echo "==========================================" +echo "Sky Art Shop - User Management Test" +echo "==========================================" +echo "" + +# Step 1: Login as Master Admin +echo "1. Testing Master Admin Login..." +curl -s -c "$COOKIE_FILE" -X POST "$BASE_URL/admin/login" \ + -d "email=admin@skyartshop.com" \ + -d "password=Admin123!" \ + -L | grep -q "Dashboard" && echo "✓ Master Admin login successful" || echo "✗ Master Admin login failed" + +echo "" + +# Step 2: Access User Management Page +echo "2. Testing User Management Access..." +curl -s -b "$COOKIE_FILE" "$BASE_URL/admin/users" | grep -q "User Management" && echo "✓ User Management page accessible" || echo "✗ User Management page access failed" + +echo "" + +# Step 3: Test Create User Page +echo "3. Testing Create User Page..." +curl -s -b "$COOKIE_FILE" "$BASE_URL/admin/users/create" | grep -q "Create New User" && echo "✓ Create User page accessible" || echo "✗ Create User page access failed" + +echo "" + +# Step 4: Test Role Permissions +echo "4. Checking Role Permissions..." +PGPASSWORD='SkyArt2025Pass!' psql -h localhost -U skyartapp -d skyartshop -t -c "SELECT DISTINCT role FROM adminusers;" | while read role; do + if [ -n "$role" ]; then + echo " - Role: $role" + fi +done + +echo "" + +# Step 5: Verify Database Structure +echo "5. Verifying Database Structure..." +PGPASSWORD='SkyArt2025Pass!' psql -h localhost -U skyartapp -d skyartshop -t -c "SELECT column_name FROM information_schema.columns WHERE table_name = 'adminusers' AND column_name IN ('passwordneverexpires', 'passwordexpiresat');" | grep -q "passwordneverexpires" && echo "✓ Password expiration fields exist" || echo "✗ Password expiration fields missing" + +echo "" + +# Step 6: Display Current Users +echo "6. Current Users in System:" +PGPASSWORD='SkyArt2025Pass!' psql -h localhost -U skyartapp -d skyartshop -c "SELECT email, name, role, isactive, passwordneverexpires FROM adminusers;" + +echo "" +echo "==========================================" +echo "Test Summary" +echo "==========================================" +echo "" +echo "Master Admin Credentials:" +echo " Email: admin@skyartshop.com" +echo " Password: Admin123!" +echo "" +echo "Access URLs:" +echo " Admin Panel: $BASE_URL/admin" +echo " User Management: $BASE_URL/admin/users" +echo " Create User: $BASE_URL/admin/users/create" +echo "" +echo "Available Roles:" +echo " - MasterAdmin (Full system access + user management)" +echo " - Admin (Full access except user management)" +echo " - Cashier (Orders, payments, customers)" +echo " - Accountant (Reports, finances, view-only)" +echo "" +echo "Features:" +echo " ✓ Role-based permissions" +echo " ✓ Password never expires option" +echo " ✓ User activation/deactivation" +echo " ✓ Secure PBKDF2 password hashing" +echo " ✓ Last login tracking" +echo " ✓ Master Admin protection (cannot be deleted)" +echo "" diff --git a/test-user-system.sh b/test-user-system.sh new file mode 100755 index 0000000..7ecda79 --- /dev/null +++ b/test-user-system.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +echo "============================================" +echo "Testing User Management System" +echo "============================================" +echo "" + +echo "1. Master Admin Login Test" +echo "-------------------------" +echo "Email: admin@skyartshop.com" +echo "Password: ChangeThisPassword123!" +echo "" + +echo "2. Checking Identity Database" +echo "-----------------------------" +sqlite3 /var/www/SkyArtShop/publish/identity.db << 'EOF' +.mode column +.headers on +SELECT UserName, Email, DisplayName FROM AspNetUsers; +EOF +echo "" + +echo "3. Checking MongoDB AdminUsers" +echo "------------------------------" +mongosh SkyArtShopDB --eval "db.AdminUsers.find({}, {Name: 1, Email: 1, Role: 1, IsActive: 1}).forEach(function(u){ print('Name: ' + u.Name + ', Email: ' + u.Email + ', Role: ' + u.Role + ', Active: ' + u.IsActive); });" --quiet +echo "" + +echo "4. User Management URLs" +echo "----------------------" +echo "List Users: http://192.168.10.130/admin/users" +echo "Create User: http://192.168.10.130/admin/users/create" +echo "Admin Login: http://192.168.10.130/admin/login" +echo "" + +echo "============================================" +echo "How to Create a New User:" +echo "============================================" +echo "1. Login as Master Admin" +echo "2. Go to User Management in sidebar" +echo "3. Click 'Add New User'" +echo "4. Fill in all required fields:" +echo " - Name, Email, Password (min 6 chars)" +echo " - Select Role (MasterAdmin/Admin/Cashier/Accountant)" +echo " - Set Status (Active/Inactive)" +echo "5. Click 'Create User'" +echo "6. User can immediately login with their credentials" +echo "" + +echo "Password Change:" +echo "----------------" +echo "1. Go to User Management" +echo "2. Click Edit button for the user" +echo "3. Enter new password in 'New Password' field" +echo "4. Leave blank to keep current password" +echo "5. Click 'Update User'" +echo "6. Password is immediately updated in BOTH databases" +echo "" + +echo "Both Systems Synchronized:" +echo "--------------------------" +echo "✓ ASP.NET Identity (SQLite) - For authentication/login" +echo "✓ MongoDB AdminUsers - For profile, roles, permissions" +echo "✓ Changes in admin panel update BOTH systems" +echo "============================================" diff --git a/verify-auth.sh b/verify-auth.sh new file mode 100755 index 0000000..ab9fd0b --- /dev/null +++ b/verify-auth.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +# SkyArt Shop - System Status & Verification Tool +# Checks authentication, database, and service health + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo "" +echo "==========================================" +echo " SkyArt Shop - System Verification" +echo "==========================================" +echo "" + +# Check service status +echo -e "${BLUE}[1/6] Service Status${NC}" +if systemctl is-active --quiet skyartshop.service; then + echo -e " ${GREEN}✓${NC} Service is running" + UPTIME=$(systemctl show skyartshop.service --property=ActiveEnterTimestamp --value) + echo " Uptime: $UPTIME" +else + echo -e " ${RED}✗${NC} Service is NOT running" + exit 1 +fi +echo "" + +# Check MongoDB +echo -e "${BLUE}[2/6] MongoDB Connection${NC}" +if mongosh --quiet --eval "db.version()" SkyArtShopDB >/dev/null 2>&1; then + MONGO_VERSION=$(mongosh --quiet --eval "db.version()" SkyArtShopDB 2>/dev/null) + echo -e " ${GREEN}✓${NC} MongoDB connected (version $MONGO_VERSION)" +else + echo -e " ${RED}✗${NC} MongoDB connection failed" + exit 1 +fi +echo "" + +# Check admin user +echo -e "${BLUE}[3/6] Admin User${NC}" +ADMIN_EXISTS=$(mongosh SkyArtShopDB --quiet --eval 'db.AdminUsers.countDocuments({Email: "admin@skyartshop.com"})' 2>/dev/null) +if [ "$ADMIN_EXISTS" = "1" ]; then + echo -e " ${GREEN}✓${NC} Admin user exists" + ADMIN_ROLE=$(mongosh SkyArtShopDB --quiet --eval 'db.AdminUsers.findOne({Email: "admin@skyartshop.com"}, {Role: 1, _id: 0}).Role' 2>/dev/null) + echo " Role: $ADMIN_ROLE" +else + echo -e " ${RED}✗${NC} Admin user NOT found" + echo " Run: ./reset-admin-password.sh to create" +fi +echo "" + +# Check DataProtection keys +echo -e "${BLUE}[4/6] DataProtection Keys${NC}" +KEYS_DIR="/var/www/SkyArtShop/publish/DataProtection-Keys" +if [ -d "$KEYS_DIR" ]; then + KEY_COUNT=$(ls -1 "$KEYS_DIR"/*.xml 2>/dev/null | wc -l) + if [ "$KEY_COUNT" -gt 0 ]; then + echo -e " ${GREEN}✓${NC} $KEY_COUNT key(s) found" + LATEST_KEY=$(ls -t "$KEYS_DIR"/*.xml 2>/dev/null | head -1 | xargs basename) + echo " Latest: $LATEST_KEY" + else + echo -e " ${YELLOW}⚠${NC} No keys found (will be generated)" + fi +else + echo -e " ${YELLOW}⚠${NC} Keys directory doesn't exist" + echo " Run: ./fix-cookies.sh to create" +fi +echo "" + +# Test web endpoint +echo -e "${BLUE}[5/6] Web Endpoint${NC}" +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://skyarts.ddns.net/admin/login -k) +if [ "$HTTP_CODE" = "200" ]; then + echo -e " ${GREEN}✓${NC} Login page accessible (HTTP $HTTP_CODE)" +else + echo -e " ${RED}✗${NC} Login page returned HTTP $HTTP_CODE" +fi +echo "" + +# Test authentication +echo -e "${BLUE}[6/6] Authentication Test${NC}" +rm -f /tmp/test-cookies.txt +curl -c /tmp/test-cookies.txt -b /tmp/test-cookies.txt \ + -X POST "https://skyarts.ddns.net/admin/login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "email=admin@skyartshop.com&password=Admin123!" \ + -k -s -o /dev/null + +DASHBOARD_TITLE=$(curl -b /tmp/test-cookies.txt "https://skyarts.ddns.net/admin/dashboard" -k -s 2>/dev/null | grep -oP '.*?') +rm -f /tmp/test-cookies.txt + +if [[ "$DASHBOARD_TITLE" == *"Dashboard"* ]]; then + echo -e " ${GREEN}✓${NC} Login successful" + echo " Dashboard accessible: $DASHBOARD_TITLE" +else + echo -e " ${RED}✗${NC} Login failed or dashboard inaccessible" + echo " Try: ./reset-admin-password.sh Admin123!" +fi +echo "" + +# Summary +echo "==========================================" +echo -e "${GREEN}System Status: Operational${NC}" +echo "==========================================" +echo "" +echo "Quick Access:" +echo " • Admin Panel: https://skyarts.ddns.net/admin/login" +echo " • Default Login: admin@skyartshop.com / Admin123!" +echo "" +echo "Management Commands:" +echo " • Reset password: ./reset-admin-password.sh [password]" +echo " • Fix cookies: ./fix-cookies.sh" +echo " • Service logs: sudo journalctl -u skyartshop.service -f" +echo " • Restart service: sudo systemctl restart skyartshop.service" +echo "" diff --git a/verify-deployment.sh b/verify-deployment.sh new file mode 100755 index 0000000..fd24630 --- /dev/null +++ b/verify-deployment.sh @@ -0,0 +1,175 @@ +#!/bin/bash + +# Sky Art Shop Deployment Verification Script +# This script verifies that the website is running correctly + +echo "============================================" +echo "Sky Art Shop - Deployment Verification" +echo "============================================" +echo "" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to check service status +check_service() { + local service=$1 + if systemctl is-active --quiet $service; then + echo -e "${GREEN}✓${NC} $service is running" + return 0 + else + echo -e "${RED}✗${NC} $service is NOT running" + return 1 + fi +} + +# Function to check port +check_port() { + local port=$1 + local service=$2 + if ss -tlnp | grep -q ":$port "; then + echo -e "${GREEN}✓${NC} Port $port is listening ($service)" + return 0 + else + echo -e "${RED}✗${NC} Port $port is NOT listening ($service)" + return 1 + fi +} + +# 1. Check Services +echo "1. Checking Services Status..." +echo "-----------------------------------" +check_service mongod +check_service nginx +check_service skyartshop.service +echo "" + +# 2. Check Ports +echo "2. Checking Ports..." +echo "-----------------------------------" +check_port 27017 "MongoDB" +check_port 80 "Nginx" +check_port 5000 "SkyArtShop App" +echo "" + +# 3. Check MongoDB Connection +echo "3. Checking MongoDB Connection..." +echo "-----------------------------------" +if mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; then + echo -e "${GREEN}✓${NC} MongoDB is accessible" + + # Check SkyArtShopDB + if mongosh --eval "use SkyArtShopDB; db.stats()" --quiet > /dev/null 2>&1; then + echo -e "${GREEN}✓${NC} SkyArtShopDB database exists" + + # Count collections + COLLECTIONS=$(mongosh SkyArtShopDB --eval "db.getCollectionNames().length" --quiet) + echo -e "${GREEN}✓${NC} Found $COLLECTIONS collections in database" + + # Show collection counts + echo "" + echo " Collection Data:" + mongosh SkyArtShopDB --eval " + print(' - Products: ' + db.Products.countDocuments()); + print(' - Pages: ' + db.Pages.countDocuments()); + print(' - MenuItems: ' + db.MenuItems.countDocuments()); + print(' - Users: ' + db.Users.countDocuments()); + print(' - SiteSettings: ' + db.SiteSettings.countDocuments()); + print(' - HomepageSections: ' + db.HomepageSections.countDocuments()); + print(' - PortfolioCategories: ' + db.PortfolioCategories.countDocuments()); + " --quiet 2>/dev/null + else + echo -e "${RED}✗${NC} SkyArtShopDB database not found" + fi +else + echo -e "${RED}✗${NC} Cannot connect to MongoDB" +fi +echo "" + +# 4. Check Web Application +echo "4. Checking Web Application..." +echo "-----------------------------------" +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost) +if [ "$HTTP_CODE" = "200" ]; then + echo -e "${GREEN}✓${NC} Website is responding (HTTP $HTTP_CODE)" +else + echo -e "${RED}✗${NC} Website returned HTTP $HTTP_CODE" +fi + +# Test admin login page +ADMIN_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/admin) +if [ "$ADMIN_CODE" = "200" ] || [ "$ADMIN_CODE" = "302" ]; then + echo -e "${GREEN}✓${NC} Admin panel is accessible" +else + echo -e "${YELLOW}!${NC} Admin panel returned HTTP $ADMIN_CODE" +fi +echo "" + +# 5. Check Network Configuration +echo "5. Network Configuration..." +echo "-----------------------------------" +IP_ADDRESS=$(hostname -I | awk '{print $1}') +echo -e "${GREEN}✓${NC} Server IP: $IP_ADDRESS" + +# Check nginx configuration +NGINX_SERVER_NAME=$(grep -E "^\s*server_name" /etc/nginx/sites-enabled/skyartshop 2>/dev/null | awk '{print $2}' | tr -d ';') +if [ -z "$NGINX_SERVER_NAME" ] || [ "$NGINX_SERVER_NAME" = "_" ]; then + echo -e "${YELLOW}!${NC} Nginx is configured for ANY domain (server_name _)" + echo " Access via: http://$IP_ADDRESS" +else + echo -e "${GREEN}✓${NC} Nginx configured for: $NGINX_SERVER_NAME" + echo " Access via: http://$NGINX_SERVER_NAME" +fi +echo "" + +# 6. Check Application Files +echo "6. Application Files..." +echo "-----------------------------------" +if [ -f "/var/www/SkyArtShop/publish/SkyArtShop.dll" ]; then + echo -e "${GREEN}✓${NC} Application binary exists" +else + echo -e "${RED}✗${NC} Application binary NOT found" +fi + +if [ -f "/var/www/SkyArtShop/appsettings.Production.json" ]; then + echo -e "${GREEN}✓${NC} Production settings exist" +else + echo -e "${RED}✗${NC} Production settings NOT found" +fi + +UPLOAD_DIR="/var/www/SkyArtShop/wwwroot/uploads" +if [ -d "$UPLOAD_DIR" ]; then + UPLOAD_COUNT=$(find $UPLOAD_DIR -type f 2>/dev/null | wc -l) + echo -e "${GREEN}✓${NC} Uploads directory exists ($UPLOAD_COUNT files)" +else + echo -e "${YELLOW}!${NC} Uploads directory not found" +fi +echo "" + +# 7. Recent Application Logs +echo "7. Recent Application Logs..." +echo "-----------------------------------" +if journalctl -u skyartshop.service -n 5 --no-pager 2>/dev/null | tail -5; then + echo "" +else + echo -e "${YELLOW}!${NC} Unable to read application logs" +fi + +# Summary +echo "" +echo "============================================" +echo "Verification Complete!" +echo "============================================" +echo "" +echo "Access your website at:" +echo " - http://$IP_ADDRESS" +if [ ! -z "$NGINX_SERVER_NAME" ] && [ "$NGINX_SERVER_NAME" != "_" ]; then + echo " - http://$NGINX_SERVER_NAME" +fi +echo "" +echo "Admin Panel:" +echo " - http://$IP_ADDRESS/admin" +echo "" diff --git a/verify-mongodb-connection.sh b/verify-mongodb-connection.sh new file mode 100755 index 0000000..f8f56c0 --- /dev/null +++ b/verify-mongodb-connection.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ MongoDB Connection Verification ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" + +# 1. Check MongoDB Service +echo "[1/5] MongoDB Service Status" +if systemctl is-active --quiet mongod; then + echo " ✅ MongoDB service is running" + UPTIME=$(systemctl show mongod --property=ActiveEnterTimestamp --value) + echo " 📅 Uptime: $UPTIME" +else + echo " ❌ MongoDB service is NOT running" + exit 1 +fi +echo "" + +# 2. Check MongoDB Port +echo "[2/5] MongoDB Network Binding" +if sudo ss -tulpn | grep -q 27017; then + echo " ✅ MongoDB listening on port 27017" + BIND=$(sudo ss -tulpn | grep 27017 | awk '{print $5}') + echo " 🔌 Binding: $BIND" +else + echo " ❌ MongoDB NOT listening on port 27017" + exit 1 +fi +echo "" + +# 3. Test Direct Connection +echo "[3/5] Direct MongoDB Connection Test" +if mongosh --quiet --eval "db.version()" SkyArtShopDB >/dev/null 2>&1; then + VERSION=$(mongosh --quiet --eval "db.version()" SkyArtShopDB 2>/dev/null) + echo " ✅ Can connect to MongoDB" + echo " 📦 Version: $VERSION" + + COLLECTIONS=$(mongosh --quiet --eval "db.getCollectionNames().length" SkyArtShopDB 2>/dev/null) + echo " 📚 Collections: $COLLECTIONS" + + PRODUCTS=$(mongosh --quiet --eval "db.Products.countDocuments()" SkyArtShopDB 2>/dev/null) + echo " 🛍️ Products: $PRODUCTS" +else + echo " ❌ Cannot connect to MongoDB" + exit 1 +fi +echo "" + +# 4. Check Application Configuration +echo "[4/5] Application MongoDB Configuration" +if [ -f /var/www/SkyArtShop/publish/appsettings.Production.json ]; then + CONN_STRING=$(grep -A 1 "ConnectionString" /var/www/SkyArtShop/publish/appsettings.Production.json | grep -oP '(?<=": ")[^"]+') + DB_NAME=$(grep -A 2 "MongoDB" /var/www/SkyArtShop/publish/appsettings.Production.json | grep "DatabaseName" | grep -oP '(?<=": ")[^"]+') + + echo " ✅ Configuration file found" + echo " 🔗 Connection: $CONN_STRING" + echo " 💾 Database: $DB_NAME" +else + echo " ❌ Configuration file NOT found" + exit 1 +fi +echo "" + +# 5. Test Website Data Retrieval +echo "[5/5] Website Data Retrieval Test" +PRODUCTS_ON_PAGE=$(curl -s https://skyarts.ddns.net/Shop 2>&1 | grep -c "product-card") +if [ "$PRODUCTS_ON_PAGE" -gt 0 ]; then + echo " ✅ Website retrieving data from MongoDB" + echo " 📦 Products displayed: $PRODUCTS_ON_PAGE" + + # Test admin dashboard + curl -c /tmp/mongo-test.txt -b /tmp/mongo-test.txt -X POST "https://skyarts.ddns.net/admin/login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "email=admin@skyartshop.com&password=Admin123!" \ + -k -s -o /dev/null 2>&1 + + if curl -b /tmp/mongo-test.txt "https://skyarts.ddns.net/admin/products" -k -s 2>&1 | grep -q "product"; then + echo " ✅ Admin backend accessing MongoDB" + fi + rm -f /tmp/mongo-test.txt +else + echo " ❌ Website NOT retrieving data" +fi +echo "" + +# Summary +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ ✅ CONNECTION VERIFIED ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" +echo "Architecture:" +echo " 🖥️ Linux Server: $(hostname)" +echo " 🌐 Web Application: ASP.NET Core 8.0 (localhost:5000)" +echo " 💾 MongoDB: Local (127.0.0.1:27017)" +echo " 🔒 Nginx Proxy: Public (skyarts.ddns.net:443)" +echo "" +echo "Data Flow:" +echo " Browser → Nginx (SSL) → ASP.NET App → MongoDB" +echo " (All running on the same Linux server)" +echo "" +echo "✅ Website is properly connected to MongoDB on this Linux server" +echo "" diff --git a/verify-system.bat b/verify-system.bat new file mode 100755 index 0000000..b500f19 --- /dev/null +++ b/verify-system.bat @@ -0,0 +1,49 @@ +@echo off +echo ============================================ +echo Sky Art Shop - Full System Verification +echo ============================================ +echo. + +echo [1/5] Checking if backend is running... +powershell -Command "$proc = Get-Process | Where-Object {$_.ProcessName -like '*SkyArtShop*'}; if ($proc) { Write-Host ' ✓ Backend is running (PID: ' $proc.Id ')' -ForegroundColor Green } else { Write-Host ' ✗ Backend is NOT running' -ForegroundColor Red; Write-Host ' Start with: cd Admin; dotnet run --launch-profile https' -ForegroundColor Yellow }" +echo. + +echo [2/5] Testing API endpoint... +powershell -Command "try { $products = Invoke-RestMethod -Uri 'http://localhost:5000/api/products' -ErrorAction Stop; Write-Host ' ✓ API working - Found' $products.Count 'products' -ForegroundColor Green } catch { Write-Host ' ✗ API not responding: ' $_.Exception.Message -ForegroundColor Red }" +echo. + +echo [3/5] Checking product images... +powershell -Command "$products = Invoke-RestMethod -Uri 'http://localhost:5000/api/products' 2>$null; foreach ($p in $products) { if ($p.images.Count -gt 0) { Write-Host ' ✓' $p.name '- HAS' $p.images.Count 'images' -ForegroundColor Green } else { Write-Host ' ✗' $p.name '- NO images (upload needed)' -ForegroundColor Yellow } }" +echo. + +echo [4/5] Testing image serving... +powershell -Command "try { $img = Invoke-WebRequest -Uri 'http://localhost:5000/uploads/products/2dbdad6c-c4a6-4f60-a1ce-3ff3b88a13ae.jpg' -Method Head -ErrorAction Stop; Write-Host ' ✓ Images are accessible via HTTP' -ForegroundColor Green } catch { Write-Host ' ⚠ Test image not found (might be OK if you have different images)' -ForegroundColor Yellow }" +echo. + +echo [5/5] Checking demo files... +if exist "shop-demo.html" ( + echo ✓ shop-demo.html exists +) else ( + echo ✗ shop-demo.html missing +) +if exist "js\api-integration.js" ( + echo ✓ api-integration.js exists +) else ( + echo ✗ api-integration.js missing +) +if exist "css\api-styles.css" ( + echo ✓ api-styles.css exists +) else ( + echo ✗ api-styles.css missing +) +echo. + +echo ============================================ +echo NEXT STEPS: +echo ============================================ +echo 1. Open demo: shop-demo.html +echo 2. Add images: https://localhost:5001/admin/products +echo 3. Integrate: See SHOP_HTML_INTEGRATION.html +echo ============================================ +echo. +pause diff --git a/verify-system.sh b/verify-system.sh new file mode 100755 index 0000000..68f527b --- /dev/null +++ b/verify-system.sh @@ -0,0 +1,171 @@ +#!/bin/bash +# SkyArtShop System Verification Script +# Verifies MongoDB, images, and application status + +echo "=========================================" +echo "SkyArtShop System Verification" +echo "=========================================" +echo "" + +# Color codes +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check MongoDB +echo "1. MongoDB Service Status" +echo "-----------------------------------------" +if systemctl is-active --quiet mongod; then + echo -e "${GREEN}✓ MongoDB is running${NC}" +else + echo -e "${RED}✗ MongoDB is not running${NC}" +fi + +# Check MongoDB connection +echo "" +echo "2. MongoDB Database Check" +echo "-----------------------------------------" +DB_CHECK=$(mongosh --quiet --eval "db.adminCommand('ping').ok" 2>&1) +if [ "$DB_CHECK" = "1" ]; then + echo -e "${GREEN}✓ MongoDB connection successful${NC}" +else + echo -e "${RED}✗ Cannot connect to MongoDB${NC}" +fi + +# Check collections +echo "" +echo "3. MongoDB Collections" +echo "-----------------------------------------" +mongosh --quiet SkyArtShopDB --eval ' +db.getCollectionNames().forEach(function(col) { + var count = db.getCollection(col).countDocuments(); + print(col + ": " + count + " documents"); +})' + +# Check products and their images +echo "" +echo "4. Product Data Integrity" +echo "-----------------------------------------" +MISSING_IMAGES=0 +mongosh --quiet SkyArtShopDB --eval ' +var products = db.Products.find({}).toArray(); +products.forEach(function(p) { + print("Product: " + p.Name); + print(" ID: " + p._id); + print(" Price: $" + p.Price); + print(" ImageUrl: " + (p.ImageUrl || "MISSING")); + print(" Images: " + (p.Images ? p.Images.length : 0) + " images"); + print(" Colors: " + (p.Colors ? p.Colors.join(", ") : "none")); + print(" Active: " + p.IsActive); + print(""); +}); +' + +# Check if images exist on filesystem +echo "" +echo "5. Image File Verification" +echo "-----------------------------------------" +IMAGES_DIR="/var/www/SkyArtShop/wwwroot/uploads/images" +if [ -d "$IMAGES_DIR" ]; then + IMAGE_COUNT=$(find "$IMAGES_DIR" -type f | wc -l) + echo -e "${GREEN}✓ Images directory exists${NC}" + echo " Total images: $IMAGE_COUNT" + + # Check product images + mongosh --quiet SkyArtShopDB --eval ' + var products = db.Products.find({}).toArray(); + products.forEach(function(p) { + if (p.ImageUrl) { + var filename = p.ImageUrl.split("/").pop(); + print(filename); + } + if (p.Images) { + p.Images.forEach(function(img) { + var filename = img.split("/").pop(); + print(filename); + }); + } + }); + ' | while read -r img; do + if [ -n "$img" ] && [ ! -f "$IMAGES_DIR/$img" ]; then + echo -e "${RED} ✗ Missing: $img${NC}" + MISSING_IMAGES=$((MISSING_IMAGES + 1)) + fi + done + + if [ $MISSING_IMAGES -eq 0 ]; then + echo -e "${GREEN} ✓ All product images exist${NC}" + fi +else + echo -e "${RED}✗ Images directory not found${NC}" +fi + +# Check application status +echo "" +echo "6. Application Status" +echo "-----------------------------------------" +if pgrep -f "SkyArtShop.dll" > /dev/null; then + echo -e "${GREEN}✓ Application is running${NC}" + APP_PID=$(pgrep -f "SkyArtShop.dll") + echo " Process ID: $APP_PID" + + # Check if port is listening + if ss -tln | grep -q ":5001"; then + echo -e "${GREEN}✓ Application listening on port 5001${NC}" + else + echo -e "${YELLOW}⚠ Application not listening on expected port${NC}" + fi +else + echo -e "${RED}✗ Application is not running${NC}" +fi + +# Check Nginx +echo "" +echo "7. Web Server Status" +echo "-----------------------------------------" +if systemctl is-active --quiet nginx; then + echo -e "${GREEN}✓ Nginx is running${NC}" + if ss -tln | grep -q ":80"; then + echo -e "${GREEN}✓ Nginx listening on port 80${NC}" + fi +else + echo -e "${RED}✗ Nginx is not running${NC}" +fi + +# Test website response +echo "" +echo "8. Website Response Test" +echo "-----------------------------------------" +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5001/ 2>&1) +if [ "$HTTP_CODE" = "200" ]; then + echo -e "${GREEN}✓ Website responding correctly (HTTP 200)${NC}" +elif [ "$HTTP_CODE" = "000" ]; then + echo -e "${RED}✗ Cannot connect to website${NC}" +else + echo -e "${YELLOW}⚠ Website returned HTTP $HTTP_CODE${NC}" +fi + +# Test shop page +SHOP_TEST=$(curl -s http://localhost:5001/shop 2>&1 | grep -c "product-card") +if [ "$SHOP_TEST" -gt 0 ]; then + echo -e "${GREEN}✓ Shop page displaying $SHOP_TEST products${NC}" +else + echo -e "${YELLOW}⚠ Shop page may have issues${NC}" +fi + +# Disk space check +echo "" +echo "9. Disk Space" +echo "-----------------------------------------" +df -h /var/www/SkyArtShop | tail -1 | awk '{print " Used: " $3 " / " $2 " (" $5 ")"}' + +# Summary +echo "" +echo "=========================================" +echo "Verification Complete" +echo "=========================================" +echo "" +echo "All critical systems checked." +echo "If any issues were found, please review the output above." +echo "" diff --git a/verify-website.sh b/verify-website.sh new file mode 100755 index 0000000..9af993c --- /dev/null +++ b/verify-website.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +echo "==========================================" +echo " SkyArt Shop - Website Verification" +echo "==========================================" +echo "" + +# Test Homepage +echo "[1/5] Testing Homepage..." +HOME_SECTIONS=$(curl -s https://skyarts.ddns.net/ | grep -c "hero\|inspiration") +if [ "$HOME_SECTIONS" -gt 0 ]; then + echo " ✓ Homepage sections loading ($HOME_SECTIONS found)" +else + echo " ✗ Homepage sections NOT loading" +fi + +# Test Shop Page +echo "" +echo "[2/5] Testing Shop Page..." +PRODUCT_COUNT=$(curl -s https://skyarts.ddns.net/Shop | grep -c "product-card") +if [ "$PRODUCT_COUNT" -gt 0 ]; then + echo " ✓ Products displaying ($PRODUCT_COUNT products found)" +else + echo " ✗ Products NOT displaying" +fi + +# Test Navigation +echo "" +echo "[3/5] Testing Navigation..." +NAV_COUNT=$(curl -s https://skyarts.ddns.net/ | grep -A 30 "nav-menu" | grep -c "
  • ") +echo " ✓ Navigation items: $NAV_COUNT" + +# Test Database +echo "" +echo "[4/5] Testing Database..." +DB_PRODUCTS=$(mongosh SkyArtShopDB --quiet --eval "db.Products.countDocuments()") +DB_PAGES=$(mongosh SkyArtShopDB --quiet --eval "db.Pages.countDocuments()") +DB_MENU=$(mongosh SkyArtShopDB --quiet --eval "db.MenuItems.countDocuments()") +echo " ✓ Products in DB: $DB_PRODUCTS" +echo " ✓ Pages in DB: $DB_PAGES" +echo " ✓ Menu items in DB: $DB_MENU" + +# Test Admin Backend +echo "" +echo "[5/5] Testing Admin Backend..." +curl -c /tmp/admin-verify.txt -b /tmp/admin-verify.txt -X POST "https://skyarts.ddns.net/admin/login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "email=admin@skyartshop.com&password=Admin123!" \ + -k -s -o /dev/null + +ADMIN_TITLE=$(curl -b /tmp/admin-verify.txt "https://skyarts.ddns.net/admin/dashboard" -k -s | grep -oP '.*?') +rm -f /tmp/admin-verify.txt + +if [[ "$ADMIN_TITLE" == *"Dashboard"* ]]; then + echo " ✓ Admin backend accessible" +else + echo " ✗ Admin backend NOT accessible" +fi + +echo "" +echo "==========================================" +echo "✓ Website Verification Complete" +echo "==========================================" +echo "" +echo "Visit: https://skyarts.ddns.net/" +echo "Admin: https://skyarts.ddns.net/admin/login" +echo "" diff --git a/wwwroot/assets/css/main.css b/wwwroot/assets/css/main.css new file mode 100755 index 0000000..e98107c --- /dev/null +++ b/wwwroot/assets/css/main.css @@ -0,0 +1,4223 @@ +/* ==================================== + Force Light Mode - Override Browser Dark Mode + ==================================== */ +:root { + color-scheme: light !important; +} + +html { + color-scheme: light !important; + background-color: #ffffff !important; +} + +html, +body { + background-color: #FAF8F5 !important; + color: #000000 !important; + color-scheme: light !important; +} + +/* Force all elements to light mode */ +* { + color-scheme: light !important; +} + +/* Override any dark backgrounds */ +.container, +.row, +.col, +div, +section, +article, +main, +header { + background-color: inherit !important; +} + +/* Allow footer to have custom background */ +footer.footer { + background-color: #1a1a1a !important; +} + +/* Ensure form elements are light */ +input, +textarea, +select, +button { + background-color: #ffffff !important; + color: #000000 !important; + border-color: #dee2e6 !important; +} + +input::placeholder, +textarea::placeholder { + color: #6c757d !important; +} + +/* ==================================== + Custom Scrollbar Styles + ==================================== */ +::-webkit-scrollbar { + width: 4px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.15); + border-radius: 2px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.25); +} + +/* Firefox scrollbar */ +* { + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.15) transparent; +} + +/* ==================================== + Notification Animations + ==================================== */ +@keyframes slideInFromTop { + from { + opacity: 0; + transform: translateY(-20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideOut { + from { + opacity: 1; + transform: translateY(0); + } + + to { + opacity: 0; + transform: translateY(-20px); + } +} + +/* ==================================== + Product Detail - Modern + ==================================== */ +.product-detail-modern { + padding: 40px 0; +} + +.gallery { + background: #fff; + padding: 16px; + display: flex; + gap: 16px; +} + +.gallery-sidebar { + display: flex; + flex-direction: column; + gap: 10px; +} + +.gallery-main { + position: relative; + background: #fafafa; + border: 1px solid #e5e5e5; + border-radius: 8px; + padding: 16px; + display: flex; + align-items: center; + justify-content: center; + min-height: 560px; + flex: 1; + overflow: hidden; + cursor: pointer; +} + +.gallery-main img { + width: 100%; + height: 100%; + object-fit: contain; + display: block; + transition: opacity 200ms ease; +} + +.gallery-main img.fade-out { + opacity: 0; +} + +.gallery-main .nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: rgba(0, 0, 0, 0.5); + color: #fff; + border: none; + width: 36px; + height: 36px; + border-radius: 18px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.2s ease; +} + +.gallery-main .nav:hover { + background: rgba(0, 0, 0, 0.7); +} + +.gallery-main .prev { + left: 8px; +} + +.gallery-main .next { + right: 8px; +} + +.gallery-thumbs { + display: flex; + flex-direction: column; + gap: 8px; +} + +.gallery-thumbs .thumb { + width: 70px; + height: 70px; + border: 2px solid #e5e5e5; + border-radius: 6px; + overflow: hidden; + cursor: pointer; + transition: all 0.3s ease; + background: #fff; + flex-shrink: 0; +} + +.gallery-thumbs .thumb img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.gallery-thumbs .thumb:hover { + border-color: #6B4E9B; +} + +.gallery-thumbs .thumb.active { + border-color: #6B4E9B; + box-shadow: 0 0 0 3px rgba(107, 78, 155, 0.2); +} + +/* Only apply opacity filter when a variant is selected */ +.variant-filtering .gallery-thumbs .thumb.variant-match { + opacity: 1; +} + +.variant-filtering .gallery-thumbs .thumb:not(.variant-match) { + opacity: 0.3; + filter: grayscale(50%); +} + +.zoom-hint { + text-align: left; + color: #777; + font-size: 0.8rem; + width: 70px; + line-height: 1.3; + margin-top: 4px; +} + +/* Disable zoom cursor/animations on touch devices */ +@media (pointer: coarse) { + .gallery-main { + cursor: default; + } + + .gallery-main img { + transition: opacity 200ms ease; + } +} + +/* Product split layout */ +.product-detail-modern .product-split { + display: flex; + gap: 24px; + align-items: flex-start; +} + +.product-detail-modern .image-pane { + flex: 0 0 625px; + max-width: 625px; +} + +.product-detail-modern .info-pane { + flex: 1 1 auto; + min-width: 0; +} + +@media (max-width: 991.98px) { + .product-detail-modern .product-split { + flex-direction: column; + } + + .product-detail-modern .image-pane { + max-width: 100%; + } +} + +/* Mobile button stack */ +@media (max-width: 576px) { + .actions { + flex-direction: column; + } + + .actions .cta { + width: 100%; + } +} + +/* ============================= + Lightbox Viewer + ============================= */ +.lightbox { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.85); + display: none; + align-items: center; + justify-content: center; + z-index: 1050; +} + +.lightbox.open { + display: flex; +} + +.lightbox-content { + position: relative; + max-width: 92vw; + max-height: 92vh; +} + +.lightbox-content img { + max-width: 92vw; + max-height: 92vh; + object-fit: contain; + background: #111; + border-radius: 8px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); +} + +.lightbox .lb-close { + position: absolute; + top: -40px; + right: -10px; + color: #fff; + font-size: 1.8rem; + background: transparent; +} + +.lightbox .lb-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: rgba(0, 0, 0, 0.5); + color: #fff; + width: 44px; + height: 44px; + border-radius: 22px; + display: flex; + align-items: center; + justify-content: center; +} + +.lightbox .lb-prev { + left: -56px; +} + +.lightbox .lb-next { + right: -56px; +} + +@media (max-width: 768px) { + .lightbox .lb-prev { + left: -36px; + } + + .lightbox .lb-next { + right: -36px; + } + + .lightbox .lb-close { + top: -36px; + right: 0; + } +} + +.details { + padding: 10px 24px; +} + +.details .title { + font-size: 2rem; + font-weight: 700; + color: #333; + margin-bottom: 12px; +} + +.details .meta { + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #e5e5e5; + padding-bottom: 12px; + margin-bottom: 16px; +} + +.details .meta-left { + display: flex; + flex-direction: column; + gap: 6px; +} + +.details .sku { + color: #666; + font-size: 0.9rem; +} + +.details .stars { + color: #FFB800; + font-size: 1rem; + display: flex; + align-items: center; + gap: 2px; +} + +.details .stars .rating-text { + color: #666; + font-size: 0.85rem; + margin-left: 6px; +} + +.details .units-sold { + background: #f0f8ff; + color: #4169E1; + padding: 4px 10px; + border-radius: 12px; + font-size: 0.85rem; + font-weight: 600; +} + +.color-section { + margin: 16px 0 12px; +} + +.color-row { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + background: #f8f8f8; + border-radius: 6px; + cursor: pointer; + transition: background 0.2s ease; + position: relative; +} + +.color-row:hover { + background: #f0f0f0; +} + +.color-row .label { + font-weight: 600; + color: #333; + font-size: 0.95rem; +} + +.color-row .value { + color: #6B4E9B; + font-weight: 600; + flex: 1; +} + +.color-row .color-arrow { + color: #666; + font-size: 0.9rem; + transition: transform 0.3s ease; +} + +.color-section:hover .color-arrow { + transform: rotate(180deg); +} + +.color-section.show .color-arrow { + transform: rotate(180deg); +} + +.swatches { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); + gap: 8px; + background: #fafafa; + border: 1px solid #e5e5e5; + border-radius: 6px; + padding: 10px; + margin-top: 8px; + max-height: 0; + overflow: hidden; + opacity: 0; + transition: all 0.3s ease; +} + +.color-section:hover .swatches { + max-height: 500px; + opacity: 1; +} + +.color-section.show .swatches { + max-height: 500px; + opacity: 1; +} + +.swatch { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + border: 2px solid #e0e0e0; + border-radius: 20px; + background: #fff; + cursor: pointer; + transition: all 0.2s ease; +} + +.swatch:hover { + border-color: #6B4E9B; + transform: translateY(-1px); +} + +.swatch.active { + border-color: #6B4E9B; + box-shadow: 0 0 0 2px rgba(107, 78, 155, 0.15); + background: #f4effa; +} + +.swatch .dot { + width: 16px; + height: 16px; + border-radius: 50%; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.swatch .name { + font-size: 0.85rem; + color: #333; + font-weight: 500; +} + +/* Variant System Styles */ +.variant-section { + margin: 20px 0; +} + +.variant-swatches { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 12px; +} + +.variant-swatch { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 8px; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + position: relative; + min-width: 70px; +} + +.variant-swatch:hover { + background: #f5f5f5; + transform: translateY(-2px); +} + +.variant-swatch.selected { + background: #f4effa; +} + +.variant-dot { + width: 32px; + height: 32px; + border-radius: 50%; + transition: box-shadow 0.2s ease; + cursor: pointer; +} + +.variant-swatch:hover .variant-dot { + transform: scale(1.1); +} + +.variant-name { + font-size: 0.8rem; + color: #333; + font-weight: 500; + text-align: center; + max-width: 80px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.variant-badge { + position: absolute; + top: 2px; + right: 2px; + background: #ff6b6b; + color: white; + font-size: 0.65rem; + padding: 2px 6px; + border-radius: 10px; + font-weight: 600; + white-space: nowrap; +} + +.variant-badge.out-of-stock { + background: #999; +} + +.short-description { + font-size: 0.95rem; + color: #666; + margin-top: 8px; + margin-bottom: 12px; + line-height: 1.5; +} + +.price-row { + display: flex; + align-items: baseline; + gap: 8px; + border-top: 1px solid #e5e5e5; + padding-top: 12px; + padding-bottom: 4px; + margin-top: 6px; + margin-bottom: -6px; +} + +.price-row .label { + color: #555; + font-size: 1rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.price-row .price { + font-size: 1.8rem; + font-weight: 700; + color: #2C2C2C; + font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; +} + +.stock-row { + margin: 0px 0 10px; +} + +.stock { + font-size: 1rem; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; +} + +.stock.ok { + color: #28a745; +} + +.stock.bad { + color: #dc3545; +} + +.stock-bar { + height: 6px; + border-radius: 3px; + width: 100%; +} + +.stock-bar.green { + background: #28a745; +} + +.stock-bar.red { + background: #dc3545; +} + +.qty-row { + margin: 16px 0 20px; +} + +.qty-row .qty-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.qty-row .label { + font-weight: 600; + color: #333; + font-size: 0.95rem; +} + +.qty-row .stock-count { + font-size: 0.85rem; + color: #666; + font-weight: 500; +} + +.qty { + display: inline-flex; + align-items: center; + border: 1px solid #e0e0e0; + border-radius: 8px; + overflow: hidden; + background: #fff; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); +} + +.qty .qty-btn { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.85rem; + color: #555; + background: #f5f5f5; + border: none; + cursor: pointer; + transition: all 0.2s ease; + border-radius: 4px; +} + +.qty .qty-btn:hover:not(:disabled) { + background: #6B4E9B; + color: #fff; + transform: scale(1.05); +} + +.qty .qty-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.qty .qty-btn i { + line-height: 1; +} + +.qty input { + width: 45px; + height: 28px; + text-align: center; + border: none; + border-left: 1px solid #e0e0e0; + border-right: 1px solid #e0e0e0; + font-weight: 600; + font-size: 0.9rem; + font-size: 0.95rem; + color: #333; + background: #fff; +} + +.actions { + display: flex; + gap: 12px; + margin-top: 14px; +} + +.actions .cta { + flex: 1; + padding: 16px 30px; + border: none; + border-radius: 8px; + font-weight: 700; + background: linear-gradient(135deg, #FF6B9D 0%, #C2185B 100%); + color: #fff; + display: inline-flex; + box-shadow: 0 4px 15px rgba(194, 24, 91, 0.3); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + align-items: center; + justify-content: center; + gap: 10px; + box-shadow: var(--shadow-sm); + transition: var(--transition); +} + +.actions .cta:hover { + transform: translateY(-2px) scale(1.02); + box-shadow: 0 6px 25px rgba(194, 24, 91, 0.45); + background: linear-gradient(135deg, #FF7BA8 0%, #D81B60 100%); +} + +.actions .cta.alt { + background: linear-gradient(135deg, #8E24AA 0%, #6A1B9A 100%); + box-shadow: 0 4px 15px rgba(142, 36, 170, 0.3); +} + +.actions .cta.alt:hover { + background: linear-gradient(135deg, #9C27B0 0%, #7B1FA2 100%); + box-shadow: 0 6px 25px rgba(142, 36, 170, 0.45); + transform: translateY(-2px) scale(1.02); +} + +.actions .cta i { + font-size: 1.05rem; +} + +.short { + margin-top: 16px; + padding: 16px 18px; + background: #f8f9fa; + border-left: 4px solid #6B4E9B; + border-radius: 6px; + font-family: 'Roboto', 'Segoe UI', sans-serif; + font-size: 0.95rem; + color: #555; + line-height: 1.6; +} + +/* Description Container - Modern Tabbed Box */ +.description-container { + margin-top: 32px; + margin-bottom: 24px; +} + +.description-tab { + background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%); + color: white; + padding: 14px 20px; + font-size: 1.1rem; + font-weight: 600; + border-radius: 8px 8px 0 0; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 2px 4px rgba(124, 58, 237, 0.2); + transition: all 0.3s ease; + user-select: none; +} + +.description-tab:hover { + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); + box-shadow: 0 4px 8px rgba(124, 58, 237, 0.3); +} + +.description-tab i { + font-size: 1.2rem; + transition: transform 0.3s ease; +} + +.description-box { + border: 2px solid #e8e8e8; + border-top: none; + border-radius: 0 0 8px 8px; + background: white; + overflow: hidden; + transition: all 0.4s ease; + max-height: 2000px; + opacity: 1; +} + +.description-box.closed { + max-height: 0; + opacity: 0; + border: none; +} + +.description-content { + padding: 24px; + color: #4a4a4a; + line-height: 1.8; + font-family: 'Roboto', 'Segoe UI', sans-serif; + font-size: 1rem; + max-height: none; + overflow: hidden; + transition: max-height 0.4s ease; +} + +.description-content.collapsed { + max-height: 250px; + position: relative; +} + +.description-content.collapsed::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 80px; + background: linear-gradient(to bottom, transparent, white); + pointer-events: none; +} + +.description-content p { + margin-bottom: 14px; +} + +.description-content h4 { + font-size: 1.15rem; + font-weight: 600; + color: #333; + margin: 20px 0 10px; +} + +.description-content ul, +.description-content ol { + padding-left: 24px; + margin-bottom: 14px; +} + +.description-content li { + margin-bottom: 8px; +} + +.see-more-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + width: 100%; + padding: 12px; + border: none; + border-top: 1px solid #e8e8e8; + background: #f8f9fa; + color: #7c3aed; + font-weight: 600; + font-size: 0.95rem; + cursor: pointer; + transition: all 0.3s ease; +} + +.see-more-btn:hover { + background: #7c3aed; + color: white; +} + +.see-more-btn i { + font-size: 1rem; + transition: transform 0.3s ease; +} + +/* Legacy desc-block for backwards compatibility */ +.desc-block { + margin-top: 32px; + padding: 0; + background: transparent; + border: none; + border-radius: 0; +} + +.desc-block h3 { + font-size: 1.5rem; + font-weight: 600; + color: #2C2C2C; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 2px solid #e8e8e8; + font-family: 'Roboto', 'Segoe UI', sans-serif; + letter-spacing: -0.5px; +} + +.desc-block .content { + color: #4a4a4a; + line-height: 1.8; + font-family: 'Roboto', 'Segoe UI', sans-serif; + font-size: 1rem; + padding: 8px 0; +} + +.desc-block .content p { + margin-bottom: 14px; +} + +.desc-block .content h4 { + font-size: 1.15rem; + font-weight: 600; + color: #333; + margin: 20px 0 10px; +} + +.desc-block .content ul, +.desc-block .content ol { + padding-left: 24px; + margin-bottom: 14px; +} + +.desc-block .content li { + margin-bottom: 8px; +} + +/* Related Products Cards - Matches Shop Page Style */ +.product-link { + text-decoration: none; + color: inherit; + display: block; +} + +/* ==================================== + Sky Art Shop - Main Stylesheet + ==================================== */ + +/* ==================================== + CSS Variables + ==================================== */ +:root { + --primary-color: #6B4E9B; + --primary-dark: #563D7C; + --secondary-color: #E91E63; + --accent-color: #FF9800; + --text-color: #2C3E50; + --text-light: #5A6C7D; + --bg-color: #FFFFFF; + --bg-light: #F8F9FA; + --bg-dark: #2C2C2C; + --border-color: #E1E8ED; + --success-color: #4CAF50; + --error-color: #F44336; + + --font-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; + --font-heading: 'Georgia', 'Times New Roman', serif; + + --transition-fast: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.12); + --shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.15); + + --container-width: 1200px; + --spacing-xs: 0.5rem; + --spacing-sm: 1rem; + --spacing-md: 2rem; + --spacing-lg: 3rem; + --spacing-xl: 4rem; +} + +/* ==================================== + Reset & Base Styles + ==================================== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + scroll-behavior: smooth; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + height: 100%; +} + +body { + font-family: var(--font-primary); + color: var(--text-color); + background-color: var(--bg-color); + line-height: 1.7; + font-size: 16px; + font-weight: 400; + letter-spacing: 0.01em; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +main { + flex: 1 0 auto; +} + +.footer { + flex-shrink: 0; +} + +img { + max-width: 100%; + height: auto; + display: block; +} + +a { + text-decoration: none; + color: inherit; + transition: var(--transition-fast); +} + +a:hover { + opacity: 0.85; +} + +ul { + list-style: none; +} + +button { + cursor: pointer; + border: none; + background: none; + font-family: inherit; + transition: var(--transition-fast); +} + +button:focus-visible, +a:focus-visible, +input:focus-visible, +textarea:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; + border-radius: 4px; +} + +/* ==================================== + Typography - Default styles that respect inline styles from CKEditor + ==================================== */ +h1:not([style*="margin"]):not([style*="line-height"]), +h2:not([style*="margin"]):not([style*="line-height"]), +h3:not([style*="margin"]):not([style*="line-height"]), +h4:not([style*="margin"]):not([style*="line-height"]), +h5:not([style*="margin"]):not([style*="line-height"]), +h6:not([style*="margin"]):not([style*="line-height"]) { + font-family: var(--font-heading); + line-height: 1.2; + margin-bottom: var(--spacing-sm); + font-weight: 600; +} + +h1:not([style*="font-size"]) { + font-size: 2.5rem; +} + +h2:not([style*="font-size"]) { + font-size: 2rem; +} + +h3:not([style*="font-size"]) { + font-size: 1.5rem; +} + +p:not([style*="margin"]) { + margin-bottom: var(--spacing-sm); +} + +/* ==================================== + Container & Layout + ==================================== */ +.container { + max-width: var(--container-width); + margin: 0 auto; + padding: 0 var(--spacing-md); +} + +/* ==================================== + Navigation Bar + ==================================== */ +.navbar { + background-color: var(--bg-color); + box-shadow: var(--shadow-sm); + position: sticky; + top: 0; + z-index: 1000; +} + +.navbar-content { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 2rem; + position: relative; +} + +.nav-brand { + flex-shrink: 0; +} + +.nav-brand a { + text-decoration: none; + display: flex; + align-items: center; + gap: 12px; +} + +.logo-image { + width: 50px; + height: 50px; + object-fit: cover; + border-radius: 50%; +} + +.nav-brand h1 { + font-size: 1.8rem; + color: var(--primary-color); + margin: 0; +} + +.nav-center { + position: absolute; + left: 50%; + transform: translateX(-50%); + display: flex; + pointer-events: none; +} + +.nav-center .nav-menu { + pointer-events: auto; +} + +.nav-menu { + display: flex; + gap: 2rem; + align-items: center; + list-style: none; + margin: 0; + padding: 0; +} + +.nav-menu li { + margin: 0; + padding: 0; +} + +.nav-menu a { + color: var(--text-color); + font-weight: 500; + transition: var(--transition); + position: relative; + white-space: nowrap; + text-decoration: none; + padding: 0.5rem 0; + display: block; +} + +.nav-menu a:hover, +.nav-menu a.active { + color: var(--primary-color); +} + +.nav-menu a.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background-color: var(--primary-color); +} + +.nav-icons { + display: flex; + align-items: center; + gap: 1rem; + flex-shrink: 0; +} + +.nav-icon { + position: relative; + color: var(--text-color); + font-size: 1.5rem; + transition: var(--transition); + text-decoration: none; + display: flex; + align-items: center; +} + +.nav-icon:hover { + color: var(--primary-color); +} + +.nav-icon .badge { + position: absolute; + top: -8px; + right: -8px; + background-color: var(--primary-color); + color: white; + font-size: 0.7rem; + font-weight: 600; + padding: 2px 6px; + border-radius: 10px; + min-width: 18px; + text-align: center; + line-height: 1; + display: none; +} + +/* Cart and Wishlist Dropdown */ +.dropdown-container { + position: relative; +} + +.icon-dropdown { + display: none; + position: absolute; + top: calc(100% + 15px); + right: 0; + background: white; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + width: 350px; + max-height: 450px; + z-index: 1002; + animation: dropdownFadeIn 0.2s ease; +} + +.icon-dropdown.show { + display: block; +} + +@keyframes dropdownFadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.dropdown-header { + padding: 15px 20px; + border-bottom: 1px solid #eee; +} + +.dropdown-header h4 { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: var(--text-color); +} + +.dropdown-items { + max-height: 300px; + overflow-y: auto; + padding: 10px 0; +} + +.dropdown-items .empty-message { + padding: 40px 20px; + text-align: center; + color: #999; + font-size: 0.9rem; +} + +.dropdown-item { + display: flex; + gap: 12px; + padding: 12px 20px; + border-bottom: 1px solid #f5f5f5; + transition: background 0.2s; +} + +.dropdown-item:hover { + background: #fafafa; +} + +.dropdown-item:last-child { + border-bottom: none; +} + +.dropdown-item-image { + width: 60px; + height: 60px; + object-fit: cover; + border-radius: 6px; + flex-shrink: 0; +} + +.dropdown-item-info { + flex: 1; + min-width: 0; +} + +.dropdown-item-name { + font-size: 0.9rem; + font-weight: 500; + color: var(--text-color); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dropdown-item-details { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.85rem; + color: #666; +} + +.dropdown-item-quantity { + color: #999; +} + +.dropdown-item-price { + font-weight: 600; + color: var(--primary-color); +} + +.dropdown-item-remove { + background: none; + border: none; + color: #999; + cursor: pointer; + padding: 4px; + font-size: 1.2rem; + line-height: 1; + transition: color 0.2s; +} + +.dropdown-item-remove:hover { + color: #e74c3c; +} + +.dropdown-footer { + padding: 15px 20px; + border-top: 1px solid #eee; +} + +.dropdown-total { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 600; + font-size: 1rem; + margin-bottom: 12px; + color: var(--text-color); +} + +.dropdown-total span:last-child { + color: var(--primary-color); + font-size: 1.1rem; +} + +.btn-checkout, +.btn-view-all { + display: block; + width: 100%; + padding: 10px; + text-align: center; + background: var(--primary-color) !important; + color: white !important; + text-decoration: none !important; + border-radius: 6px; + font-weight: 500; + transition: all 0.3s ease; + transform: scale(1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.btn-checkout:hover, +.btn-view-all:hover { + background: var(--primary-color) !important; + color: white !important; + text-decoration: none !important; + transform: scale(1.05); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.btn-checkout:focus, +.btn-view-all:focus, +.btn-checkout:active, +.btn-view-all:active, +.btn-checkout:visited, +.btn-view-all:visited { + background: var(--primary-color) !important; + color: white !important; + text-decoration: none !important; +} + +.nav-toggle { + display: flex; + flex-direction: column; + gap: 5px; + background: none; + border: none; + cursor: pointer; + padding: 10px; + flex-shrink: 0; +} + +.nav-toggle span { + width: 25px; + height: 3px; + background-color: var(--text-color); + transition: var(--transition); +} + +.nav-toggle:hover span { + background-color: var(--primary-color); +} + +.nav-dropdown { + display: none; + position: absolute; + top: 100%; + right: 2rem; + background-color: white; + box-shadow: var(--shadow-md); + border-radius: 8px; + min-width: 250px; + z-index: 1001; + margin-top: 0; +} + +.nav-dropdown.active { + display: block; +} + +.nav-dropdown .nav-menu { + flex-direction: column; + padding: var(--spacing-sm); + gap: 0; +} + +.nav-dropdown .nav-menu li { + width: 100%; + border-bottom: 1px solid #f0f0f0; +} + +.nav-dropdown .nav-menu li:last-child { + border-bottom: none; +} + +.nav-dropdown .nav-menu a { + display: block; + padding: 12px 16px; + width: 100%; + text-align: left; +} + +.nav-dropdown .nav-menu a:hover { + background-color: #f5f5f5; +} + +.nav-dropdown .nav-menu a.active::after { + display: none; +} + +/* ==================================== + Hero Section + ==================================== */ +.hero { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-lg); + align-items: center; + padding: var(--spacing-xl) var(--spacing-md); + background: linear-gradient(135deg, #fce4ec 0%, #f8bbd0 50%, #f48fb1 100%); + min-height: 550px; + position: relative; + overflow: hidden; +} + +.hero::before { + content: ''; + position: absolute; + top: -50%; + right: -20%; + width: 100%; + height: 100%; + background: radial-gradient(circle, rgba(244, 143, 177, 0.15) 0%, transparent 70%); + z-index: 0; +} + +.hero>* { + position: relative; + z-index: 1; +} + +.hero-content { + padding: 0 var(--spacing-md); +} + +.hero-content h2 { + font-size: 3rem; + color: #c2185b; + margin-bottom: var(--spacing-md); + line-height: 1.2; + text-shadow: 2px 2px 4px rgba(255, 255, 255, 0.8); + background: transparent !important; + padding: 0; +} + +.hero-content p { + font-size: 1.2rem; + color: #5d4037; + margin-bottom: var(--spacing-md); + background: transparent !important; + padding: 0 !important; + text-shadow: 1px 1px 3px rgba(255, 255, 255, 0.7); +} + +.hero-content .hero-description { + font-size: 1rem; + color: #424242; + margin-bottom: var(--spacing-md); + line-height: 1.8; +} + +.hero-content .hero-description p:not([style]) { + margin-bottom: 1rem; +} + +.hero-content .hero-description ul:not([style]), +.hero-content .hero-description ol:not([style]) { + margin-left: 1.5rem; + margin-bottom: 1rem; +} + +.hero-content .hero-description h1:not([style]), +.hero-content .hero-description h2:not([style]), +.hero-content .hero-description h3:not([style]) { + margin-top: 1.5rem; + margin-bottom: 0.75rem; +} + +.hero-image { + position: relative; + overflow: hidden; + border-radius: 10px; + padding-right: 2rem; +} + +.hero-image img { + width: 100%; + height: 500px; + object-fit: cover; + transition: transform 0.5s ease; +} + +.hero-image:hover img { + transform: scale(1.05); +} + +/* ==================================== + Buttons + ==================================== */ +.btn { + display: inline-block; + padding: 12px 30px; + border-radius: 8px; + font-weight: 600; + text-align: center; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + border: none; + font-family: var(--font-primary); + letter-spacing: 0.02em; +} + +.btn-primary { + background: linear-gradient(135deg, #f06292 0%, #ec407a 100%); + color: white; + box-shadow: 0 2px 8px rgba(236, 64, 122, 0.3); +} + +.btn-primary:hover { + background: linear-gradient(135deg, #ec407a 0%, #e91e63 100%); + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(236, 64, 122, 0.4); +} + +.btn-secondary { + background: linear-gradient(135deg, #f48fb1 0%, #f06292 100%); + color: white; +} + +.btn-secondary:hover { + background: linear-gradient(135deg, #f06292 0%, #ec407a 100%); + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(240, 98, 146, 0.3); +} + +.btn-small { + padding: 8px 20px; + font-size: 0.9rem; +} + +.btn-icon { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 16px; +} + +.btn-icon svg { + pointer-events: none; + flex-shrink: 0; +} + +/* ==================================== + Sections + ==================================== */ +section { + padding: var(--spacing-xl) 0; +} + +.section-subtitle { + text-align: center; + font-size: 1.1rem; + color: var(--text-light); + margin-bottom: var(--spacing-lg); +} + +/* ==================================== + Inspiration Section + ==================================== */ +.inspiration { + background-color: var(--bg-light); + padding: var(--spacing-xl) 0; +} + +.inspiration h2 { + text-align: center; + color: var(--primary-color); + margin-bottom: var(--spacing-lg); +} + +.inspiration-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-lg); + align-items: center; +} + +.inspiration-text p { + font-size: 1.1rem; + line-height: 1.8; + color: var(--text-color); + margin-bottom: 1rem; +} + +.inspiration-image { + border-radius: 10px; + overflow: hidden; + box-shadow: var(--shadow-md); +} + +.inspiration-image img { + width: 100%; + height: 400px; + object-fit: cover; +} + +/* ==================================== + Collection Section + ==================================== */ +.collection { + text-align: center; +} + +.collection h2 { + color: var(--primary-color); + margin-bottom: var(--spacing-sm); +} + +.collection-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: var(--spacing-md); + margin: var(--spacing-lg) 0; +} + +.collection-item { + background-color: white; + border-radius: 10px; + overflow: hidden; + box-shadow: var(--shadow-sm); + transition: var(--transition); + cursor: pointer; +} + +.collection-item:hover { + transform: translateY(-10px); + box-shadow: var(--shadow-lg); +} + +.collection-item img { + width: 100%; + height: 250px; + object-fit: cover; +} + +.collection-item h3 { + padding: var(--spacing-sm); + color: var(--text-color); +} + +/* ==================================== + Promotion Section + ==================================== */ +.promotion { + background: linear-gradient(135deg, #f8bbd0 0%, #f48fb1 50%, #f06292 100%); + color: #333; + padding: var(--spacing-xl) 0; +} + +.promotion .container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--spacing-md); +} + +.promo-card { + background-color: rgba(255, 255, 255, 0.95); + padding: var(--spacing-lg); + border-radius: 15px; + text-align: center; + transition: var(--transition); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.promo-card:hover { + background-color: rgba(255, 255, 255, 1); + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); +} + +.promo-card h2 { + color: #c2185b; + margin-bottom: var(--spacing-md); +} + +.promo-description { + font-size: 1.1rem; + margin-bottom: var(--spacing-md); + color: #555; +} + +.promo-card.featured { + background: linear-gradient(135deg, rgba(236, 64, 122, 0.15), rgba(240, 98, 146, 0.15)); + border: 2px solid #ec407a; +} + +/* ==================================== + Products Grid + ==================================== */ +.products-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: var(--spacing-md); + margin-top: var(--spacing-lg); +} + +.product-card { + background-color: white; + border-radius: 12px; + overflow: hidden; + box-shadow: var(--shadow-sm); + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid var(--border-color); +} + +.product-card:hover { + box-shadow: var(--shadow-hover); + transform: translateY(-8px); + border-color: var(--primary-color); +} + +.product-image { + position: relative; + overflow: hidden; + height: 180px; +} + +.product-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.5s ease; +} + +.product-card:hover .product-image img { + transform: scale(1.1); +} + +.product-card h3 { + padding: var(--spacing-sm); + font-size: 1.1rem; +} + +.product-color-badge { + display: inline-block; + margin: 0 var(--spacing-sm) var(--spacing-xs); + padding: 0.25rem 0.75rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + background: var(--primary-color); + color: white; + border-radius: 12px; + letter-spacing: 0.5px; +} + +.product-description { + padding: 0 var(--spacing-sm); + font-size: 0.9rem; + color: var(--text-light); + margin-bottom: var(--spacing-xs); + line-height: 1.6; +} + +/* Product Description Rich Text Styles - Only if editor hasn't set custom styles */ +.product-description p:not([style]) { + margin-bottom: 0.5em; +} + +.product-description strong:not([style]), +.product-description b:not([style]) { + font-weight: bold; + color: var(--text-color); +} + +.product-description em:not([style]), +.product-description i:not([style]) { + font-style: italic; +} + +.product-description u:not([style]) { + text-decoration: underline; +} + +.product-description ul:not([style]), +.product-description ol:not([style]) { + margin: 0.5em 0 0.5em 1.2em; + padding-left: 0; +} + +.product-description li:not([style]) { + margin-bottom: 0.3em; +} + +.price { + padding: 0 var(--spacing-sm); + font-size: 1.3rem; + font-weight: 700; + color: var(--primary-color); + margin-bottom: var(--spacing-sm); +} + +.product-card .btn { + width: calc(100% - var(--spacing-md)); + margin: 0 var(--spacing-sm) var(--spacing-sm); +} + +/* ==================================== + Portfolio Section + ==================================== */ +.portfolio-hero, +.shop-hero, +.about-hero, +.contact-hero, +.category-hero { + text-align: center; + padding: 80px 0; + background: linear-gradient(135deg, #f06292 0%, #ec407a 50%, #e91e63 100%); + color: white; + margin-bottom: 0; + position: relative; + overflow: hidden; +} + +.portfolio-hero::before, +.shop-hero::before, +.about-hero::before, +.contact-hero::before, +.category-hero::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.1); + z-index: 1; +} + +.portfolio-hero>*, +.shop-hero>*, +.about-hero>*, +.contact-hero>*, +.category-hero>* { + position: relative; + z-index: 2; +} + +.portfolio-hero h1, +.shop-hero h1, +.about-hero h1, +.contact-hero h1, +.category-hero h1 { + color: #ffffff; + font-size: 3rem; + margin-bottom: var(--spacing-sm); + font-weight: 700; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); + letter-spacing: -0.5px; +} + +.hero-subtitle { + font-size: 1.2rem; + color: #ffffff; + max-width: 700px; + margin: 0 auto; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + font-weight: 400; + opacity: 0.95; +} + +/* Remove white background from containers inside hero sections */ +.portfolio-hero .container, +.shop-hero .container, +.about-hero .container, +.contact-hero .container, +.category-hero .container, +.hero .container { + background-color: transparent !important; + background: none !important; +} + +.portfolio-gallery { + padding: var(--spacing-xl) 0; +} + +.portfolio-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--spacing-lg); +} + +.portfolio-category { + position: relative; + overflow: hidden; + border-radius: 10px; + box-shadow: var(--shadow-md); + height: 400px; +} + +.category-link { + display: block; + height: 100%; +} + +.category-image { + position: relative; + height: 100%; + overflow: hidden; +} + +.category-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.5s ease; +} + +.category-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent); + padding: var(--spacing-md); + color: white; + transition: var(--transition); +} + +.category-overlay h2 { + color: white; + margin: 0; +} + +.portfolio-category:hover .category-image img { + transform: scale(1.1); +} + +.portfolio-category:hover .category-overlay { + background: linear-gradient(to top, rgba(107, 78, 155, 0.9), transparent); +} + +/* ==================================== + Breadcrumb + ==================================== */ +.breadcrumb { + padding: var(--spacing-sm) 0; + background-color: var(--bg-light); + font-size: 0.9rem; +} + +.breadcrumb a { + color: var(--primary-color); + transition: var(--transition); +} + +.breadcrumb a:hover { + text-decoration: underline; +} + +.breadcrumb span { + color: var(--text-light); +} + +/* ==================================== + Projects Gallery + ==================================== */ +.projects-gallery { + padding: var(--spacing-xl) 0; +} + +.projects-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: var(--spacing-lg); +} + +.project-card { + background-color: white; + border-radius: 10px; + overflow: hidden; + box-shadow: var(--shadow-sm); + transition: var(--transition); +} + +.project-card:hover { + box-shadow: var(--shadow-lg); + transform: translateY(-5px); +} + +.project-image { + height: 300px; + overflow: hidden; +} + +.project-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.5s ease; +} + +.project-card:hover .project-image img { + transform: scale(1.1); +} + +.project-info { + padding: var(--spacing-md); +} + +.project-info h3 { + color: var(--primary-color); + margin-bottom: var(--spacing-xs); +} + +.project-date { + color: var(--text-light); + font-size: 0.9rem; +} + +/* ==================================== + Shop Page + ==================================== */ +.shop-filters { + background-color: var(--bg-light); + padding: var(--spacing-md) 0; +} + +.filter-bar { + display: flex; + gap: var(--spacing-md); + flex-wrap: wrap; +} + +.filter-group { + display: flex; + align-items: center; + gap: var(--spacing-xs); +} + +.filter-group label { + font-weight: 600; +} + +.filter-group select { + padding: 8px 15px; + border: 1px solid var(--border-color); + border-radius: 5px; + font-family: inherit; + cursor: pointer; +} + +.shop-products { + padding: var(--spacing-xl) 0; +} + +/* ==================================== + About Page + ==================================== */ +.about-content { + padding: var(--spacing-xl) 0; +} + +.about-layout { + display: grid; + grid-template-columns: 1fr 350px; + gap: var(--spacing-xl); +} + +.about-main-content { + min-width: 0; +} + +.content-wrapper { + max-width: 100%; +} + +/* Only apply default styles if CKEditor hasn't set inline styles */ +.content-wrapper h2:not([style]) { + color: var(--primary-color); + margin-top: var(--spacing-md); + margin-bottom: var(--spacing-sm); + font-size: 2rem; +} + +.content-wrapper h3:not([style]) { + color: var(--text-color); + margin-top: var(--spacing-md); + margin-bottom: var(--spacing-sm); + font-size: 1.5rem; +} + +.content-wrapper p:not([style]) { + margin-bottom: 1rem; + line-height: 1.8; +} + +.content-wrapper ul:not([style]) { + list-style: disc; + padding-left: 1.5rem; + margin-bottom: 1rem; +} + +.content-wrapper ol:not([style]) { + list-style: decimal; + padding-left: 1.5rem; + margin-bottom: 1rem; +} + +.content-wrapper li:not([style]) { + margin-bottom: 0.5rem; + line-height: 1.7; +} + +.content-wrapper img { + max-width: 100%; + height: auto; + border-radius: 10px; + box-shadow: var(--shadow-md); + margin: var(--spacing-md) 0; +} + +/* About Sidebar Image Gallery */ +.about-sidebar { + position: relative; + height: fit-content; +} + +.sidebar-images { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.sidebar-image-item { + border-radius: 12px; + overflow: hidden; + box-shadow: var(--shadow-md); + transition: var(--transition); +} + +.sidebar-image-item:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-hover); +} + +.sidebar-image-item img { + width: 100%; + height: auto; + display: block; + object-fit: cover; +} + +/* Team Section */ +.team-section { + padding: var(--spacing-xl) 0; + background: var(--bg-light); +} + +.section-header h2 { + color: var(--primary-color); + font-size: 2.5rem; + margin-bottom: var(--spacing-sm); +} + +.section-header .lead { + color: var(--text-muted); + font-size: 1.2rem; +} + +.team-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 300px)); + gap: var(--spacing-lg); + justify-content: center; +} + +.team-member-card { + background: white; + border-radius: 12px; + padding: var(--spacing-lg); + text-align: center; + box-shadow: var(--shadow-md); + transition: var(--transition); + display: flex; + flex-direction: column; +} + +.team-member-card:hover { + transform: translateY(-8px); + box-shadow: var(--shadow-hover); +} + +.team-member-info { + padding: 0 var(--spacing-sm); + margin-bottom: var(--spacing-xl); + flex-grow: 1; +} + +.member-name { + font-size: 1.5rem; + color: var(--text-color); + margin-bottom: var(--spacing-xs); + font-weight: 600; +} + +.member-role { + font-size: 1rem; + color: var(--primary-color); + font-weight: 500; + margin-bottom: var(--spacing-sm); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.member-bio { + font-size: 0.95rem; + color: var(--text-muted); + line-height: 1.6; + margin-bottom: var(--spacing-md); +} + +.team-member-photo { + margin-top: auto; + display: flex; + justify-content: center; +} + +.team-member-photo img { + width: 180px; + height: 180px; + border-radius: 50%; + object-fit: cover; + border: 4px solid var(--primary-color); + box-shadow: 0 4px 12px rgba(107, 78, 155, 0.2); + transition: var(--transition); +} + +.team-member-card:hover .team-member-photo img { + border-color: var(--primary-dark); + box-shadow: 0 8px 20px rgba(107, 78, 155, 0.3); + transform: scale(1.05); +} + +/* Legacy styles for fallback */ +.about-grid { + display: grid; + grid-template-columns: 2fr 1fr; + gap: var(--spacing-lg); +} + +.about-text h2 { + color: var(--primary-color); + margin-top: var(--spacing-md); + margin-bottom: var(--spacing-sm); +} + +.about-text ul { + list-style: disc; + padding-left: var(--spacing-md); + margin-bottom: var(--spacing-md); +} + +.about-text li { + margin-bottom: var(--spacing-xs); +} + +.about-images { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.about-images img { + width: 100%; + border-radius: 10px; + box-shadow: var(--shadow-md); +} + +/* Responsive Design */ +@media (max-width: 992px) { + .about-layout { + grid-template-columns: 1fr; + } + + .about-sidebar { + position: relative; + top: 0; + } + + .sidebar-images { + flex-direction: row; + flex-wrap: wrap; + gap: var(--spacing-sm); + } + + .sidebar-image-item { + flex: 1 1 calc(50% - var(--spacing-sm)); + min-width: 200px; + } + + .team-grid { + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + } +} + +/* ==================================== + Contact Page + ==================================== */ +.contact-form-section { + padding: var(--spacing-xl) 0; +} + +.contact-form-wrapper { + max-width: 700px; + margin: 0 auto; +} + +.contact-grid { + display: grid; + grid-template-columns: 1fr 2fr; + gap: var(--spacing-lg); +} + +.contact-info { + background: linear-gradient(135deg, #fce4ec 0%, #f8bbd0 100%); + padding: var(--spacing-lg); + border-radius: 15px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.contact-info h2 { + color: #c2185b; + margin-bottom: var(--spacing-md); + font-size: 1.5rem; +} + +.contact-item { + margin-bottom: var(--spacing-md); +} + +.contact-item h3 { + font-size: 1rem; + margin-bottom: var(--spacing-xs); + color: #c2185b; + font-weight: 600; +} + +.contact-item a { + color: #333; +} + +.contact-item a:hover { + color: #ec407a; +} + +.contact-form { + background-color: white; + padding: var(--spacing-xl); + border-radius: 15px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +} + +.contact-form h2 { + color: #c2185b; + margin-bottom: var(--spacing-md); +} + +.form-group { + margin-bottom: var(--spacing-md); +} + +.form-group label { + display: block; + margin-bottom: var(--spacing-xs); + font-weight: 600; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 12px; + border: 1px solid var(--border-color); + border-radius: 5px; + font-family: inherit; + font-size: 1rem; + transition: var(--transition); +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: #f06292; + box-shadow: 0 0 0 3px rgba(240, 98, 146, 0.15); +} + +/* ==================================== + Footer + ==================================== */ +.footer { + background: #1a1a1a !important; + color: #e0e0e0; + padding: var(--spacing-xl) 0 var(--spacing-md); + box-shadow: 0 -2px 20px rgba(0, 0, 0, 0.1); +} + +.footer-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-lg); + margin-bottom: var(--spacing-lg); +} + +.footer-brand h2 { + color: #ffffff; + margin-bottom: var(--spacing-sm); + font-weight: 600; +} + +.footer-brand p { + margin-bottom: var(--spacing-sm); +} + +.social-links { + display: flex; + gap: var(--spacing-sm); +} + +.social-links a { + color: white; + transition: var(--transition); +} + +.social-links a:hover { + color: var(--accent-color); + transform: translateY(-3px); +} + +.footer-links h3 { + color: white; + margin-bottom: var(--spacing-md); +} + +.footer-links ul li { + margin-bottom: var(--spacing-xs); +} + +.footer-links a { + color: #b0b0b0; + transition: var(--transition); + text-decoration: none; +} + +.footer-links a:hover { + color: #ffffff; + transform: translateX(3px); +} + +.footer-bottom { + text-align: center; + padding-top: var(--spacing-md); + border-top: 1px solid rgba(255, 255, 255, 0.1); + color: #CCCCCC; + font-size: 0.9rem; +} + +/* ==================================== + Responsive Design + ==================================== */ + +/* Tablet */ +@media (max-width: 768px) { + h1 { + font-size: 2rem; + } + + h2 { + font-size: 1.5rem; + } + + .nav-brand h1 { + font-size: 1.5rem; + } + + .nav-center { + display: none; + } + + .nav-dropdown { + display: none; + position: fixed; + left: 0; + top: 60px; + background-color: white; + width: 100%; + box-shadow: var(--shadow-md); + z-index: 999; + max-height: calc(100vh - 60px); + overflow-y: auto; + border-radius: 0; + margin-top: 0; + } + + .nav-dropdown.active { + display: block; + } + + .nav-dropdown .nav-menu { + padding: var(--spacing-md) var(--spacing-sm); + } + + .hero { + grid-template-columns: 1fr; + } + + .hero-content h2 { + font-size: 2rem; + } + + .inspiration-content { + grid-template-columns: 1fr; + } + + .collection-grid { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + } + + .portfolio-grid { + grid-template-columns: 1fr; + } + + .products-grid { + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + } + + .about-grid { + grid-template-columns: 1fr; + } + + .contact-grid { + grid-template-columns: 1fr; + } + + .footer-content { + grid-template-columns: 1fr; + } +} + +/* Mobile */ +@media (max-width: 480px) { + :root { + --spacing-md: 1rem; + --spacing-lg: 2rem; + --spacing-xl: 2.5rem; + } + + .container { + padding: 0 var(--spacing-sm); + } + + h1 { + font-size: 1.8rem; + } + + .hero-content h2 { + font-size: 1.6rem; + } + + .portfolio-hero h1, + .shop-hero h1, + .about-hero h1, + .contact-hero h1, + .category-hero h1 { + font-size: 2rem; + } + + .collection-grid, + .products-grid, + .projects-grid { + grid-template-columns: 1fr; + } + + .filter-bar { + flex-direction: column; + } + + .filter-group { + width: 100%; + } + + .filter-group select { + width: 100%; + } + + .page-hero h1 { + font-size: 2.2rem !important; + } + + .hero-subtitle { + font-size: 1.2rem !important; + } + + .content-wrapper { + font-size: 1rem; + } + + .content-wrapper h1 { + font-size: 1.8rem !important; + } + + .content-wrapper h2 { + font-size: 1.5rem !important; + } + + .content-wrapper h3 { + font-size: 1.3rem !important; + } + + .content-wrapper h4 { + font-size: 1.1rem !important; + } + + .content-wrapper p { + font-size: 1rem !important; + } +} + +/* ==================================== + Page Content Styles + ==================================== */ +.page-hero { + background: linear-gradient(135deg, #EDAEF9 0%, #81B1FA 100%); + padding: 4rem 0 3rem; + text-align: center; + margin-bottom: 3rem; +} + +.page-hero h1 { + font-size: 3.5rem; + font-family: var(--font-heading); + margin-bottom: 1rem; + font-weight: 700; + color: #ffffff !important; + letter-spacing: 0.5px; +} + +.hero-subtitle { + font-size: 1.5rem; + color: #ffffff !important; + opacity: 0.95; + font-weight: 400; + line-height: 1.6; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); +} + +.page-content { + padding: 2rem 0 4rem; +} + +.content-wrapper { + max-width: 900px; + margin: 0 auto; + line-height: 1.8; + color: var(--text-color); + white-space: pre-wrap; + word-wrap: break-word; +} + +/* Rich Text Editor Content Styles */ +.content-wrapper h1, +.content-wrapper h2, +.content-wrapper h3, +.content-wrapper h4, +.content-wrapper h5, +.content-wrapper h6 { + margin-top: 1.5em; + margin-bottom: 0.75em; + line-height: 1.3; + color: var(--primary-color); +} + +.content-wrapper h1 { + font-size: 2.5rem; + font-weight: 700; +} + +.content-wrapper h2 { + font-size: 2rem; + font-weight: 600; +} + +.content-wrapper h3 { + font-size: 1.7rem; + font-weight: 600; +} + +.content-wrapper h4 { + font-size: 1.4rem; + font-weight: 600; +} + +.content-wrapper h5 { + font-size: 1.2rem; + font-weight: 600; +} + +.content-wrapper h6 { + font-size: 1.1rem; + font-weight: 600; +} + +.content-wrapper p { + margin-bottom: 1.2em; + font-size: 1.1rem; + line-height: 1.8; +} + +.content-wrapper ul, +.content-wrapper ol { + margin: 1em 0 1.5em 2em; + padding-left: 0; +} + +/* Preserve inline styles from pasted content */ +.content-wrapper [style*=\"font-weight\"], +.content-wrapper [style*=\"font-style\"], +.content-wrapper [style*=\"text-decoration\"], +.content-wrapper [style*=\"color\"], +.content-wrapper [style*=\"background\"], +.content-wrapper [style*=\"font-size\"], +.content-wrapper [style*=\"text-align\"], +.content-wrapper [style*=\"margin\"], +.content-wrapper [style*=\"padding\"] { + /* Inline styles preserved */ +} + +.content-wrapper strong, +.content-wrapper b { + font-weight: bold; +} + +.content-wrapper em, +.content-wrapper i { + font-style: italic; +} + +.content-wrapper u { + text-decoration: underline; +} + +.content-wrapper s, +.content-wrapper strike { + text-decoration: line-through; +} + +.content-wrapper br { + display: block; + margin: 0.5em 0; + content: \"\"; +} + +.content-wrapper div, +.content-wrapper span { + display: inline; +} + +.content-wrapper div { + display: block; +} + +.content-wrapper li { + margin-bottom: 0.5em; + line-height: 1.6; +} + +.content-wrapper blockquote { + margin: 1.5em 0; + padding: 1em 1.5em; + background: var(--bg-light); + border-left: 4px solid var(--primary-color); + font-style: italic; + color: var(--text-light); +} + +.content-wrapper img { + max-width: 100%; + height: auto; + display: block; + margin: 1.5em auto; + border-radius: 8px; + box-shadow: var(--shadow-md); +} + +.content-wrapper a { + color: var(--primary-color); + text-decoration: none; + border-bottom: 1px solid transparent; + transition: var(--transition); +} + +.content-wrapper a:hover { + border-bottom-color: var(--primary-color); +} + +.content-wrapper table { + width: 100%; + margin: 1.5em 0; + border-collapse: collapse; + box-shadow: var(--shadow-sm); +} + +.content-wrapper table th, +.content-wrapper table td { + padding: 0.75em; + border: 1px solid var(--border-color); + text-align: left; +} + +.content-wrapper table th { + background: var(--primary-color); + color: white; + font-weight: 600; +} + +.content-wrapper table tr:nth-child(even) { + background: var(--bg-light); +} + +.content-wrapper pre { + background: var(--bg-dark); + color: #f8f8f2; + padding: 1em; + border-radius: 4px; + overflow-x: auto; + margin: 1.5em 0; + font-family: 'Courier New', monospace; + font-size: 0.9em; +} + +.content-wrapper code { + background: var(--bg-light); + padding: 0.2em 0.4em; + border-radius: 3px; + font-family: 'Courier New', monospace; + font-size: 0.9em; +} + +.content-wrapper pre code { + background: none; + padding: 0; + color: inherit; +} + +.content-wrapper hr { + margin: 2em 0; + border: none; + border-top: 2px solid var(--border-color); +} + +/* ==================================== + CKEditor Content Preservation - Allow Editor Formatting to Override CSS + ==================================== */ +/* Let inline styles from CKEditor override our CSS */ +.content-wrapper [style], +.product-description [style], +.hero-description [style], +.inspiration-text [style], +.about-text [style], +.blog-content [style], +.project-info [style] { + /* Inline styles from editor take priority - this preserves spacing, colors, fonts */ + /* No CSS property here will override inline styles */ +} + +/* Ensure consistent base spacing that can be overridden by inline styles */ +.content-wrapper p:not([style*="margin"]), +.product-description p:not([style*="margin"]), +.hero-description p:not([style*="margin"]) { + margin-bottom: 1em; +} + +.content-wrapper h1:not([style*="margin"]), +.content-wrapper h2:not([style*="margin"]), +.content-wrapper h3:not([style*="margin"]), +.content-wrapper h4:not([style*="margin"]) { + margin-top: 1.5em; + margin-bottom: 0.75em; +} + +.content-wrapper h1:first-child, +.content-wrapper h2:first-child, +.content-wrapper h3:first-child, +.content-wrapper h4:first-child, +.content-wrapper p:first-child { + margin-top: 0; +} + +/* Preserve list styling unless editor overrides */ +.content-wrapper ul:not([style]), +.product-description ul:not([style]) { + list-style: disc; + padding-left: 1.5rem; + margin-bottom: 1em; +} + +.content-wrapper ol:not([style]), +.product-description ol:not([style]) { + list-style: decimal; + padding-left: 1.5rem; + margin-bottom: 1em; +} + +/* ==================================== + Utility Classes + ==================================== */ +.text-center { + text-align: center; +} + +.mt-1 { + margin-top: var(--spacing-xs); +} + +.mt-2 { + margin-top: var(--spacing-sm); +} + +.mt-3 { + margin-top: var(--spacing-md); +} + +.mt-4 { + margin-top: var(--spacing-lg); +} + +.mb-1 { + margin-bottom: var(--spacing-xs); +} + +.mb-2 { + margin-bottom: var(--spacing-sm); +} + +.mb-3 { + margin-bottom: var(--spacing-md); +} + +.mb-4 { + margin-bottom: var(--spacing-lg); +} + +/* ==================================== + Dynamic Homepage Sections + ==================================== */ +.custom-section { + padding: 60px 0; + background: var(--bg-light); +} + +.custom-section h2 { + font-size: 2.5rem; + color: var(--primary-color); + margin-bottom: 20px; + text-align: center; +} + +.custom-section .section-subtitle { + font-size: 1.2rem; + color: var(--text-light); + text-align: center; + margin-bottom: 30px; +} + +.custom-section .img-fluid { + max-width: 100%; + height: auto; + border-radius: 8px; + box-shadow: var(--shadow-md); +} + +/* Dynamic section content styling */ +.inspiration-text p, +.custom-section p { + margin-bottom: 15px; + line-height: 1.8; +} + +.inspiration-text ul, +.custom-section ul { + margin-left: 20px; + margin-bottom: 15px; +} + +.inspiration-text li, +.custom-section li { + margin-bottom: 8px; +} + +/* ==================================== + Product Detail Page + ==================================== */ +.product-detail { + padding: var(--spacing-xl) 0; + background: #fff; +} + +/* ==================================== + PRODUCT DETAIL PAGE - CLEAN LAYOUT + ==================================== */ + +.product-detail-page { + padding: 40px 0; + background: #fff; +} + +/* LEFT COLUMN: Product Images Section (40%) */ +.product-image-section { + background: white; + padding: 20px; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.main-image-wrapper { + background: #fafafa; + border: 1px solid #e5e5e5; + border-radius: 8px; + padding: 20px 30px; + /* slightly bigger: 20px vertical, 30px horizontal */ + margin-bottom: 20px; + display: inline-block; + width: auto; + max-width: 100%; +} + +.main-product-img { + max-width: 100%; + max-height: 300px; + /* increase from 280px */ + height: auto; + display: block; + object-fit: contain; +} + +/* Thumbnail Gallery - Horizontal below */ +.thumbnail-gallery { + display: flex; + gap: 8px; + justify-content: center; + flex-wrap: wrap; + margin-bottom: 15px; +} + +.thumbnail-item-gallery { + width: 60px; + height: 60px; + border: 2px solid #e5e5e5; + border-radius: 6px; + overflow: hidden; + cursor: pointer; + transition: all 0.3s ease; + background: white; +} + +.thumbnail-item-gallery img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.thumbnail-item-gallery:hover { + border-color: #6B4E9B; + transform: translateY(-2px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.thumbnail-item-gallery.active { + border-color: #6B4E9B; + box-shadow: 0 0 0 3px rgba(107, 78, 155, 0.2); +} + +/* Zoom Tooltip */ +.zoom-tooltip { + font-size: 0.85rem; + color: #777; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + margin-bottom: 20px; +} + +.zoom-tooltip i { + font-size: 1rem; +} + +/* Description Section Below Images */ +.description-section { + background: #f9f9f9; + border: 1px solid #e5e5e5; + border-radius: 8px; + padding: 25px; + margin-top: 25px; + text-align: left; +} + +.description-title { + font-size: 1.3rem; + font-weight: 700; + color: #333; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 2px solid #6B4E9B; +} + +.description-content { + color: #555; + line-height: 1.8; + font-size: 0.95rem; +} + +.description-content p { + margin-bottom: 12px; +} + +.description-content ul, +.description-content ol { + margin-left: 25px; + margin-bottom: 12px; +} + +.description-content li { + margin-bottom: 8px; +} + +/* RIGHT COLUMN: Product Details Section (60%) */ +.product-details-section { + padding: 20px 30px; +} + +/* A. Product Title */ +.product-title-main { + font-size: 2rem; + font-weight: 700; + color: #333; + margin-bottom: 20px; + line-height: 1.3; +} + +/* B. SKU + Rating */ +.product-sku-rating { + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: 20px; + margin-bottom: 20px; + border-bottom: 1px solid #e5e5e5; +} + +.sku-badge { + font-size: 0.9rem; + color: #666; + font-weight: 500; +} + +.rating-display { + color: #FFB800; + font-size: 1.1rem; +} + +.rating-display i { + margin-left: 2px; +} + +/* C. Color Label */ +.color-label-section { + margin-bottom: 15px; +} + +.color-title { + font-size: 1rem; + color: #333; + font-weight: 500; +} + +.color-title strong { + color: #6B4E9B; + font-weight: 700; +} + +/* D. Color Selection Grid */ +.color-selection-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); + gap: 12px; + margin-bottom: 30px; + padding: 20px; + background: #fafafa; + border: 1px solid #e5e5e5; + border-radius: 8px; + max-height: 300px; + overflow-y: auto; +} + +.color-swatch { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + background: white; + border: 2px solid #e0e0e0; + border-radius: 25px; + cursor: pointer; + transition: all 0.3s ease; +} + +.color-swatch:hover:not(.inactive-swatch) { + border-color: #6B4E9B; + box-shadow: 0 2px 8px rgba(107, 78, 155, 0.2); + transform: translateY(-2px); +} + +.color-swatch.active-swatch { + border-color: #6B4E9B; + background: #f0ebf7; + box-shadow: 0 0 0 3px rgba(107, 78, 155, 0.15); +} + +.color-swatch.inactive-swatch { + opacity: 0.4; + cursor: not-allowed; +} + +.color-circle { + width: 24px; + height: 24px; + border-radius: 50%; + display: inline-block; + flex-shrink: 0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.color-name-text { + font-size: 0.9rem; + color: #333; + font-weight: 500; +} + +/* E. Price Section */ +.price-section-block { + padding: 20px 0; + margin-bottom: 20px; + border-bottom: 1px solid #e5e5e5; +} + +.price-label { + font-size: 1rem; + color: #666; + font-weight: 500; + display: block; + margin-bottom: 8px; +} + +.price-amount { + font-size: 2.5rem; + font-weight: 700; + color: #4169E1; +} + +/* F. Stock Indicator */ +.stock-indicator-section { + margin-bottom: 25px; +} + +.stock-text { + font-size: 1rem; + color: #28a745; + font-weight: 500; + margin-bottom: 10px; +} + +.stock-text.out-of-stock { + color: #dc3545; +} + +.stock-bar-green { + height: 6px; + background: #28a745; + border-radius: 3px; + width: 100%; +} + +.stock-bar-red { + height: 6px; + background: #dc3545; + border-radius: 3px; + width: 100%; +} + +/* G. Quantity Selector */ +.quantity-selector-section { + margin-bottom: 25px; +} + +.qty-label { + font-size: 1rem; + font-weight: 600; + color: #333; + display: block; + margin-bottom: 12px; +} + +.qty-selector-wrapper { + display: inline-flex; + align-items: center; + border: 2px solid #e0e0e0; + border-radius: 6px; + overflow: hidden; + background: white; +} + +.qty-minus-btn, +.qty-plus-btn { + background: white; + border: none; + width: 45px; + height: 45px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.3rem; + color: #555; + transition: all 0.3s ease; +} + +.qty-minus-btn:hover:not(:disabled), +.qty-plus-btn:hover:not(:disabled) { + background: #f5f5f5; + color: #6B4E9B; +} + +.qty-minus-btn:disabled, +.qty-plus-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.qty-number { + width: 70px; + height: 45px; + border: none; + border-left: 1px solid #e0e0e0; + border-right: 1px solid #e0e0e0; + text-align: center; + font-size: 1.1rem; + font-weight: 600; + background: white; + color: #333; +} + +/* H. Buttons Section */ +.action-buttons-section { + display: flex; + gap: 15px; + margin-bottom: 30px; +} + +.btn-add-cart { + flex: 1; + padding: 16px 30px; + font-size: 1.1rem; + font-weight: 600; + border: none; + border-radius: 6px; + background: #2C2C2C; + color: white; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.btn-add-cart:hover:not(:disabled) { + background: #1a1a1a; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.btn-add-cart:disabled { + background: #ccc; + cursor: not-allowed; +} + +.btn-add-wishlist { + flex: 1; + padding: 16px 30px; + font-size: 1.1rem; + font-weight: 600; + border: none; + border-radius: 6px; + background: #2C2C2C; + color: white; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.btn-add-wishlist:hover { + background: #1a1a1a; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +/* Short Description Box */ +.short-description-box { + padding: 20px; + background: #f9f9f9; + border: 1px solid #e5e5e5; + border-radius: 6px; + color: #555; + line-height: 1.7; +} + +.short-description-box p { + margin: 0; +} + +.product-meta { + display: flex; + align-items: center; + gap: 20px; + margin-bottom: 20px; +} + +.sku-label { + font-size: 0.85rem; + color: #777; +} + +.sku-label strong { + color: #333; + font-weight: 600; +} + +.rating-stars { + color: #FFB800; + font-size: 1rem; +} + +.rating-stars i { + margin-right: 2px; +} + +/* New Compact Product Layout Styles */ +.product-meta-compact { + display: flex; + align-items: center; + gap: 20px; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid #e0e0e0; +} + +.sku-text { + font-size: 0.9rem; + color: #777; +} + +.sku-text strong { + color: #333; + font-weight: 600; +} + +.rating-stars-compact { + color: #FFB800; + font-size: 1rem; +} + +.rating-stars-compact i { + margin-right: 2px; +} + +.product-price-large { + font-size: 2.5rem; + font-weight: 700; + color: #6B4E9B; + margin-bottom: 15px; +} + +.product-stock-info { + margin-bottom: 20px; +} + +.stock-available { + color: #28a745; + font-weight: 500; + font-size: 1rem; + display: flex; + align-items: center; + gap: 8px; +} + +.stock-available i { + font-size: 1.2rem; +} + +.stock-unavailable { + color: #dc3545; + font-weight: 500; + font-size: 1rem; + display: flex; + align-items: center; + gap: 8px; +} + +.stock-unavailable i { + font-size: 1.2rem; +} + +.product-quantity-section { + margin-bottom: 25px; +} + +.quantity-label { + font-size: 1rem; + font-weight: 600; + color: #333; + display: block; + margin-bottom: 10px; +} + +.quantity-controls { + display: inline-flex; + align-items: center; + border: 2px solid #e0e0e0; + border-radius: 6px; + overflow: hidden; +} + +.qty-btn { + background: white; + border: none; + width: 45px; + height: 45px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.3rem; + color: #555; + transition: all 0.3s ease; +} + +.qty-btn:hover:not(:disabled) { + background: #f0f0f0; + color: #6B4E9B; +} + +.qty-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.qty-input { + width: 70px; + height: 45px; + border: none; + border-left: 1px solid #e0e0e0; + border-right: 1px solid #e0e0e0; + text-align: center; + font-size: 1.1rem; + font-weight: 600; + background: white; + color: #333; +} + +.product-actions { + display: flex; + gap: 15px; + margin-bottom: 25px; +} + +.btn-cart-main { + flex: 1; + padding: 15px 30px; + font-size: 1.1rem; + font-weight: 600; + border: none; + border-radius: 6px; + background: #6B4E9B; + color: white; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} + +.btn-cart-main:hover:not(:disabled) { + background: #5a3e82; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(107, 78, 155, 0.3); +} + +.btn-cart-main:disabled { + background: #ccc; + cursor: not-allowed; +} + +.btn-wishlist-icon { + width: 55px; + height: 55px; + border: 2px solid #e0e0e0; + border-radius: 6px; + background: white; + color: #666; + font-size: 1.5rem; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.btn-wishlist-icon:hover { + border-color: #E91E63; + color: #E91E63; + background: #fff5f8; +} + +.product-color-info { + margin-bottom: 20px; + padding: 15px; + background: #f8f9fa; + border-radius: 6px; +} + +.color-label { + font-weight: 600; + color: #333; + margin-right: 10px; +} + +.color-value-badge { + display: inline-block; + padding: 5px 15px; + background: #6B4E9B; + color: white; + border-radius: 20px; + font-size: 0.9rem; + font-weight: 500; +} + +.product-short-desc { + padding: 15px; + background: #f8f9fa; + border-radius: 6px; + color: #555; + line-height: 1.6; +} + +.product-short-desc p { + margin: 0; +} + +/* Section Labels */ +.section-label { + font-size: 0.9rem; + font-weight: 700; + color: #333; + display: block; + margin-bottom: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.color-value { + font-weight: 500; + color: var(--primary-color); +} + +/* Color Grid Selection */ +.color-selection-section { + border-top: 1px solid #e5e5e5; + padding-top: 20px; +} + +.color-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(135px, 1fr)); + gap: 10px; + margin-top: 12px; +} + +.color-option { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 12px; + border: 1.5px solid #ddd; + border-radius: 20px; + cursor: pointer; + transition: all 0.25s ease; + background: white; + font-size: 0.9rem; +} + +.color-option:hover:not(.unavailable) { + border-color: var(--primary-color); + box-shadow: 0 2px 8px rgba(107, 78, 155, 0.15); +} + +.color-option.active { + border-color: #6B4E9B; + background: #f8f6fb; + box-shadow: 0 0 0 2px rgba(107, 78, 155, 0.15); + font-weight: 600; +} + +.color-circle-btn { + width: 20px; + height: 20px; + border-radius: 50%; + display: inline-block; + flex-shrink: 0; +} + +.color-name { + font-size: 0.85rem; + color: #4A5F66; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Price Section */ +.price-section { + border-top: 1px solid #e5e5e5; + padding-top: 20px; +} + +.price-display { + font-size: 2rem; + color: var(--primary-color); + font-weight: 700; + margin-top: 5px; +} + +/* Stock Section */ +.stock-section { + border-top: 1px solid #e5e5e5; + padding-top: 20px; +} + +.stock-info { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.95rem; + margin-top: 5px; +} + +.stock-info.available { + color: #28a745; +} + +.stock-info.available i { + font-size: 1.2rem; +} + +.stock-info.unavailable { + color: #dc3545; +} + +.stock-info.unavailable i { + font-size: 1.2rem; +} + +/* Quantity Controls */ +.quantity-section { + border-top: 1px solid #e5e5e5; + padding-top: 20px; +} + +.quantity-wrapper { + display: inline-flex; + align-items: center; + border: 2px solid #e0e0e0; + border-radius: 50px; + overflow: hidden; + margin-top: 5px; + background: #f8f9fa; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.qty-control-btn { + background: transparent; + border: none; + width: 45px; + height: 45px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.3rem; + color: #f06292; + transition: all 0.3s ease; + font-weight: 600; +} + +.qty-control-btn:hover:not(:disabled) { + background: #f06292; + color: white; + transform: scale(1.1); +} + +.qty-control-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.qty-display { + width: 70px; + height: 45px; + border: none; + text-align: center; + font-size: 1.1rem; + font-weight: 700; + background: transparent; + color: #2d3748; +} + +/* Action Buttons */ +.action-buttons-wrapper { + display: flex; + gap: 15px; + margin-top: 30px; +} + +.btn-add-to-cart { + flex: 1; + padding: 16px 32px; + font-size: 1.05rem; + font-weight: 600; + border: none; + border-radius: 50px; + background: linear-gradient(135deg, #f06292 0%, #ec407a 100%); + color: white; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(236, 64, 122, 0.4); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.btn-add-to-cart:hover:not(:disabled) { + transform: translateY(-3px); + box-shadow: 0 6px 20px rgba(236, 64, 122, 0.5); + background: linear-gradient(135deg, #ec407a 0%, #e91e63 100%); +} + +.btn-add-to-wishlist { + flex: 1; + padding: 16px 32px; + font-size: 1.05rem; + font-weight: 600; + border: 2px solid #f06292; + border-radius: 50px; + background: white; + color: #f06292; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.btn-add-to-wishlist:hover { + background: #f06292; + color: white; + transform: translateY(-3px); + box-shadow: 0 6px 20px rgba(240, 98, 146, 0.3); +} + +/* Description Section */ +.product-description-tabs { + border-top: 1px solid #e5e5e5; + padding-top: 30px; +} + +.description-title { + font-size: 1.5rem; + color: #4A5F66; + font-weight: 600; + margin-bottom: 20px; +} + +.description-content { + color: #666; + line-height: 1.8; + font-size: 0.95rem; +} + +.product-description-section { + background: var(--bg-light); + padding: var(--spacing-lg); + border-radius: 8px; +} + +.product-description-section .section-title { + font-size: 2rem; + color: var(--text-color); + border-bottom: 3px solid var(--primary-color); + padding-bottom: 10px; +} + +.product-full-description { + font-size: 1rem; + line-height: 1.8; + color: var(--text-color); +} + +.product-full-description p { + margin-bottom: 1rem; +} + +.product-full-description ul, +.product-full-description ol { + margin-left: 1.5rem; + margin-bottom: 1rem; +} + +.product-full-description li { + margin-bottom: 0.5rem; +} + +.product-full-description h1, +.product-full-description h2, +.product-full-description h3 { + margin-top: 1.5rem; + margin-bottom: 0.75rem; + color: var(--primary-color); +} + +/* Product Card Link Styling */ +.product-card .product-link { + text-decoration: none; + color: inherit; + display: block; +} + +.product-card .product-link:hover h3 { + color: var(--primary-color); +} + +/* ======================================== + Cart & Wishlist Icon Animations + ======================================== */ +@keyframes cartBounce { + + 0%, + 100% { + transform: scale(1); + } + + 25% { + transform: scale(1.2) rotate(-5deg); + } + + 50% { + transform: scale(1.15) rotate(5deg); + } + + 75% { + transform: scale(1.1) rotate(-3deg); + } +} + +@keyframes badgePulse { + + 0%, + 100% { + transform: scale(1); + } + + 50% { + transform: scale(1.3); + } +} + +@keyframes heartBeat { + + 0%, + 100% { + transform: scale(1); + } + + 15% { + transform: scale(1.25); + } + + 30% { + transform: scale(1.1); + } + + 45% { + transform: scale(1.3); + } + + 60% { + transform: scale(1.15); + } +} + +/* ======================================== + Success Toast Notification + ======================================== */ +.success-toast { + position: fixed; + top: 80px; + right: 20px; + background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); + color: white; + padding: 16px 24px; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(76, 175, 80, 0.4); + display: flex; + align-items: center; + gap: 12px; + font-weight: 600; + z-index: 10000; + opacity: 0; + transform: translateX(400px); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.success-toast.show { + opacity: 1; + transform: translateX(0); +} + +.success-toast i { + font-size: 1.5rem; +} + +/* ======================================== + Enhanced Card Shadows for Cream BG + ======================================== */ +.product-card, +.category-card, +.icon-box, +.card { + background: #FFFFFF; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); + transition: box-shadow 0.3s ease; +} + +.product-card:hover, +.category-card:hover { + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12); +} + +/* ======================================== + Ensure sections stand out on cream BG + ======================================== */ +.hero, +.inspiration-section, +.categories-section, +.shop-grid { + background: transparent; +} + +.container { + background: transparent !important; +} + +section { + background-color: transparent !important; +} + +/* ======================================== + Responsive Toast + ======================================== */ +@media (max-width: 768px) { + .success-toast { + top: 70px; + right: 10px; + left: 10px; + font-size: 0.9rem; + padding: 14px 20px; + } +} \ No newline at end of file diff --git a/wwwroot/assets/images/about-1.jpg b/wwwroot/assets/images/about-1.jpg new file mode 100755 index 0000000..4db8af9 Binary files /dev/null and b/wwwroot/assets/images/about-1.jpg differ diff --git a/wwwroot/assets/images/about-2.jpg b/wwwroot/assets/images/about-2.jpg new file mode 100755 index 0000000..4db8af9 Binary files /dev/null and b/wwwroot/assets/images/about-2.jpg differ diff --git a/wwwroot/assets/images/cardmaking.jpg b/wwwroot/assets/images/cardmaking.jpg new file mode 100755 index 0000000..4db8af9 Binary files /dev/null and b/wwwroot/assets/images/cardmaking.jpg differ diff --git a/wwwroot/assets/images/craft-supplies.jpg b/wwwroot/assets/images/craft-supplies.jpg new file mode 100755 index 0000000..4db8af9 Binary files /dev/null and b/wwwroot/assets/images/craft-supplies.jpg differ diff --git a/wwwroot/assets/images/hero-craft.jpg b/wwwroot/assets/images/hero-craft.jpg new file mode 100755 index 0000000..4db8af9 Binary files /dev/null and b/wwwroot/assets/images/hero-craft.jpg differ diff --git a/wwwroot/assets/images/journals.jpg b/wwwroot/assets/images/journals.jpg new file mode 100755 index 0000000..4db8af9 Binary files /dev/null and b/wwwroot/assets/images/journals.jpg differ diff --git a/wwwroot/assets/images/products/placeholder.jpg b/wwwroot/assets/images/products/placeholder.jpg new file mode 100755 index 0000000..4db8af9 Binary files /dev/null and b/wwwroot/assets/images/products/placeholder.jpg differ diff --git a/wwwroot/assets/images/products/product-1.jpg b/wwwroot/assets/images/products/product-1.jpg new file mode 100755 index 0000000..4db8af9 Binary files /dev/null and b/wwwroot/assets/images/products/product-1.jpg differ diff --git a/wwwroot/assets/images/products/product-2.jpg b/wwwroot/assets/images/products/product-2.jpg new file mode 100755 index 0000000..4db8af9 Binary files /dev/null and b/wwwroot/assets/images/products/product-2.jpg differ diff --git a/wwwroot/assets/images/products/product-3.jpg b/wwwroot/assets/images/products/product-3.jpg new file mode 100755 index 0000000..4db8af9 Binary files /dev/null and b/wwwroot/assets/images/products/product-3.jpg differ diff --git a/wwwroot/assets/images/products/product-4.jpg b/wwwroot/assets/images/products/product-4.jpg new file mode 100755 index 0000000..4db8af9 Binary files /dev/null and b/wwwroot/assets/images/products/product-4.jpg differ diff --git a/wwwroot/assets/images/stickers.jpg b/wwwroot/assets/images/stickers.jpg new file mode 100755 index 0000000..4db8af9 Binary files /dev/null and b/wwwroot/assets/images/stickers.jpg differ diff --git a/wwwroot/assets/images/washi-tape.jpg b/wwwroot/assets/images/washi-tape.jpg new file mode 100755 index 0000000..4db8af9 Binary files /dev/null and b/wwwroot/assets/images/washi-tape.jpg differ diff --git a/wwwroot/assets/js/admin.js b/wwwroot/assets/js/admin.js new file mode 100755 index 0000000..eaa8e73 --- /dev/null +++ b/wwwroot/assets/js/admin.js @@ -0,0 +1,155 @@ +// Sky Art Shop - Admin Panel Functions + +// Delete confirmation with fetch +function deleteWithConfirmation(url, itemName, redirectUrl) { + if (!confirm(`Are you sure you want to delete ${itemName}?`)) { + return false; + } + + fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.ok) { + window.location.href = redirectUrl || window.location.href; + } else { + alert("Delete failed. Please try again."); + } + }) + .catch((error) => { + alert("Error: " + error.message); + }); + + return false; +} + +// Image upload with preview +function uploadImageWithPreview(fileInputId, previewId, urlInputId) { + const input = document.getElementById(fileInputId); + const file = input.files[0]; + + if (!file) { + alert("Please select a file"); + return; + } + + const formData = new FormData(); + formData.append("file", file); + + fetch("/admin/upload/image", { + method: "POST", + body: formData, + }) + .then((response) => response.json()) + .then((result) => { + if (result.success) { + document.getElementById(urlInputId).value = result.url; + const preview = document.getElementById(previewId); + if (preview) { + preview.src = result.url; + preview.style.display = "block"; + } + showAdminNotification("Image uploaded successfully!", "success"); + } else { + showAdminNotification("Upload failed: " + result.message, "error"); + } + }) + .catch((error) => { + showAdminNotification("Upload error: " + error.message, "error"); + }); +} + +// Show admin notification +function showAdminNotification(message, type = "success") { + const notification = document.createElement("div"); + notification.className = `alert alert-${ + type === "success" ? "success" : "danger" + } alert-dismissible fade show`; + notification.style.cssText = + "position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px;"; + notification.innerHTML = ` + ${message} + + `; + + document.body.appendChild(notification); + + setTimeout(() => { + notification.classList.remove("show"); + setTimeout(() => notification.remove(), 150); + }, 3000); +} + +// Copy to clipboard +function copyToClipboard(text) { + navigator.clipboard + .writeText(text) + .then(() => { + showAdminNotification("Copied to clipboard!", "success"); + }) + .catch(() => { + showAdminNotification("Failed to copy", "error"); + }); +} + +// Auto-generate slug from title +function generateSlugFromTitle(titleInputId, slugInputId) { + const titleInput = document.getElementById(titleInputId); + const slugInput = document.getElementById(slugInputId); + + if (titleInput && slugInput) { + titleInput.addEventListener("input", function () { + if (slugInput.value === "" || slugInput.dataset.auto === "true") { + slugInput.value = titleInput.value + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, ""); + slugInput.dataset.auto = "true"; + } + }); + + slugInput.addEventListener("input", function () { + slugInput.dataset.auto = "false"; + }); + } +} + +// Form validation helper +function validateForm(formId) { + const form = document.getElementById(formId); + if (!form) return true; + + const requiredFields = form.querySelectorAll("[required]"); + let isValid = true; + + requiredFields.forEach((field) => { + if (!field.value.trim()) { + field.classList.add("is-invalid"); + isValid = false; + } else { + field.classList.remove("is-invalid"); + } + }); + + if (!isValid) { + showAdminNotification("Please fill in all required fields", "error"); + } + + return isValid; +} + +// Initialize tooltips +document.addEventListener("DOMContentLoaded", function () { + // Bootstrap tooltips + var tooltipTriggerList = [].slice.call( + document.querySelectorAll('[data-bs-toggle="tooltip"]') + ); + if (typeof bootstrap !== "undefined") { + tooltipTriggerList.map(function (tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl); + }); + } +}); diff --git a/wwwroot/assets/js/cart.js b/wwwroot/assets/js/cart.js new file mode 100755 index 0000000..c6bd3bc --- /dev/null +++ b/wwwroot/assets/js/cart.js @@ -0,0 +1,378 @@ +// Sky Art Shop - Shopping Cart Functions + +// Add item to cart +function addToCart(id, name, price, imageUrl = null) { + // Get existing cart from localStorage + let cart = JSON.parse(localStorage.getItem("cart") || "[]"); + + // Check if item already exists + const existingItem = cart.find((item) => item.id === id); + + if (existingItem) { + existingItem.quantity++; + // Update imageUrl if it was null before + if (!existingItem.imageUrl && imageUrl) { + existingItem.imageUrl = imageUrl; + } + } else { + cart.push({ id, name, price, quantity: 1, imageUrl }); + } + + // Save cart + localStorage.setItem("cart", JSON.stringify(cart)); + console.log("Cart updated:", cart); + + // Show confirmation + showCartNotification(`${name} added to cart!`); + updateCartCount(); +} + +// Remove item from cart +function removeFromCart(id) { + let cart = JSON.parse(localStorage.getItem("cart") || "[]"); + cart = cart.filter((item) => item.id !== id); + localStorage.setItem("cart", JSON.stringify(cart)); + updateCartCount(); +} + +// Update cart item quantity +function updateCartQuantity(id, quantity) { + let cart = JSON.parse(localStorage.getItem("cart") || "[]"); + const item = cart.find((item) => item.id === id); + if (item) { + item.quantity = quantity; + if (quantity <= 0) { + cart = cart.filter((item) => item.id !== id); + } + } + localStorage.setItem("cart", JSON.stringify(cart)); + updateCartCount(); +} + +// Get cart items +function getCart() { + return JSON.parse(localStorage.getItem("cart") || "[]"); +} + +// Get cart total +function getCartTotal() { + const cart = getCart(); + return cart.reduce((total, item) => total + item.price * item.quantity, 0); +} + +// Update cart count badge +function updateCartCount() { + const cart = getCart(); + const count = cart.reduce((total, item) => total + item.quantity, 0); + + // Update old badge (if exists) + const badge = document.getElementById("cart-count"); + if (badge) { + badge.textContent = count; + badge.style.display = count > 0 ? "inline" : "none"; + } + + // Update navbar cart badge + const navCartBadge = document.querySelector("#cartBtn .badge"); + if (navCartBadge) { + navCartBadge.textContent = count; + navCartBadge.style.display = count > 0 ? "block" : "none"; + } +} + +// Show cart notification +function showCartNotification(message) { + const notification = document.createElement("div"); + notification.className = "cart-notification"; + notification.textContent = message; + notification.style.cssText = ` + position: fixed; + top: 80px; + right: 20px; + background: #4CAF50; + color: white; + padding: 15px 25px; + border-radius: 5px; + box-shadow: 0 4px 6px rgba(0,0,0,0.2); + z-index: 10000; + animation: slideInFromTop 0.3s ease; + `; + + document.body.appendChild(notification); + + setTimeout(() => { + notification.style.animation = "slideOut 0.3s ease"; + setTimeout(() => notification.remove(), 300); + }, 3000); +} + +// Clear entire cart +function clearCart() { + localStorage.removeItem("cart"); + updateCartCount(); +} + +// ==================================== +// Wishlist Functions +// ==================================== + +// Add item to wishlist +function addToWishlist(id, name, price, imageUrl) { + let wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]"); + + const existingItem = wishlist.find((item) => item.id === id); + + if (existingItem) { + showWishlistNotification(`${name} is already in your wishlist!`); + return; + } + + wishlist.push({ id, name, price, imageUrl }); + localStorage.setItem("wishlist", JSON.stringify(wishlist)); + console.log("Wishlist updated:", wishlist); + + showWishlistNotification(`${name} added to wishlist!`); + updateWishlistCount(); +} + +// Remove item from wishlist +function removeFromWishlist(id) { + let wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]"); + wishlist = wishlist.filter((item) => item.id !== id); + localStorage.setItem("wishlist", JSON.stringify(wishlist)); + updateWishlistCount(); +} + +// Get wishlist items +function getWishlist() { + return JSON.parse(localStorage.getItem("wishlist") || "[]"); +} + +// Update wishlist count badge +function updateWishlistCount() { + const wishlist = getWishlist(); + const count = wishlist.length; + + const navWishlistBadge = document.querySelector("#wishlistBtn .badge"); + const wishlistIcon = document.querySelector("#wishlistBtn i"); + + if (navWishlistBadge) { + navWishlistBadge.textContent = count; + navWishlistBadge.style.display = count > 0 ? "block" : "none"; + } + + // Change heart icon based on wishlist status + if (wishlistIcon) { + if (count > 0) { + wishlistIcon.className = "bi bi-heart-fill"; + wishlistIcon.style.color = "#e74c3c"; + } else { + wishlistIcon.className = "bi bi-heart"; + wishlistIcon.style.color = ""; + } + } +} + +// Show wishlist notification +function showWishlistNotification(message) { + const notification = document.createElement("div"); + notification.className = "wishlist-notification"; + notification.textContent = message; + notification.style.cssText = ` + position: fixed; + top: 80px; + right: 20px; + background: #E91E63; + color: white; + padding: 15px 25px; + border-radius: 5px; + box-shadow: 0 4px 6px rgba(0,0,0,0.2); + z-index: 10000; + animation: slideInFromTop 0.3s ease; + `; + + document.body.appendChild(notification); + + setTimeout(() => { + notification.style.animation = "slideOut 0.3s ease"; + setTimeout(() => notification.remove(), 300); + }, 3000); +} + +// Clear entire wishlist +function clearWishlist() { + localStorage.removeItem("wishlist"); + updateWishlistCount(); +} + +// ==================================== +// Dropdown Functions +// ==================================== +// Render cart dropdown +function renderCartDropdown() { + const cart = getCart(); + const cartItems = document.getElementById("cartItems"); + const cartTotal = document.getElementById("cartTotal"); + + if (!cartItems) return; + + if (cart.length === 0) { + cartItems.innerHTML = '

    Your cart is empty

    '; + if (cartTotal) cartTotal.textContent = "$0.00"; + return; + } + + console.log("Rendering cart:", cart); + + cartItems.innerHTML = cart + .map((item) => { + const imgSrc = item.imageUrl || "/assets/images/placeholder.jpg"; + console.log("Cart item image URL:", imgSrc); + return ` + + `; + }) + .join(""); + + if (cartTotal) { + const total = getCartTotal(); + cartTotal.textContent = `$${total.toFixed(2)}`; + } +} + +// Render wishlist dropdown +function renderWishlistDropdown() { + const wishlist = getWishlist(); + const wishlistItems = document.getElementById("wishlistItems"); + + if (!wishlistItems) return; + + if (wishlist.length === 0) { + wishlistItems.innerHTML = + '

    Your wishlist is empty

    '; + return; + } + + console.log("Rendering wishlist:", wishlist); + + wishlistItems.innerHTML = wishlist + .map((item) => { + const imgSrc = item.imageUrl || "/assets/images/placeholder.jpg"; + console.log("Wishlist item image URL:", imgSrc); + return ` + + `; + }) + .join(""); +} + +// Remove from cart via dropdown +function removeFromCartDropdown(id) { + removeFromCart(id); + renderCartDropdown(); + updateCartCount(); +} + +// Remove from wishlist via dropdown +function removeFromWishlistDropdown(id) { + removeFromWishlist(id); + renderWishlistDropdown(); + updateWishlistCount(); +} + +// Toggle dropdown visibility +function toggleDropdown(dropdownId) { + const dropdown = document.getElementById(dropdownId); + if (!dropdown) return; + + // Close other dropdowns + document.querySelectorAll(".icon-dropdown").forEach((d) => { + if (d.id !== dropdownId) { + d.classList.remove("show"); + } + }); + + dropdown.classList.toggle("show"); + + // Render content when opening + if (dropdown.classList.contains("show")) { + if (dropdownId === "cartDropdown") { + renderCartDropdown(); + } else if (dropdownId === "wishlistDropdown") { + renderWishlistDropdown(); + } + } +} + +// Close cart/wishlist dropdowns when clicking outside +document.addEventListener("click", function (e) { + if ( + !e.target.closest(".dropdown-container") && + !e.target.closest(".nav-toggle") + ) { + document.querySelectorAll(".icon-dropdown").forEach((d) => { + d.classList.remove("show"); + }); + } +}); + +// Initialize cart and wishlist count on page load +document.addEventListener("DOMContentLoaded", function () { + updateCartCount(); + updateWishlistCount(); + + // Add click handlers for dropdown toggles + const cartBtn = document.getElementById("cartBtn"); + const wishlistBtn = document.getElementById("wishlistBtn"); + + if (cartBtn) { + cartBtn.addEventListener("click", function (e) { + e.preventDefault(); + toggleDropdown("cartDropdown"); + }); + } + + if (wishlistBtn) { + wishlistBtn.addEventListener("click", function (e) { + e.preventDefault(); + toggleDropdown("wishlistDropdown"); + }); + } +}); diff --git a/wwwroot/assets/js/main.js b/wwwroot/assets/js/main.js new file mode 100755 index 0000000..46e3c4a --- /dev/null +++ b/wwwroot/assets/js/main.js @@ -0,0 +1,427 @@ +// Sky Art Shop - Main JavaScript File + +// ==================================== +// Mobile Navigation Toggle +// ==================================== +document.addEventListener("DOMContentLoaded", function () { + const navToggle = document.querySelector(".nav-toggle"); + const navMenu = document.querySelector("#navDropdown"); + + if (navToggle && navMenu) { + // Hover to open dropdown + navToggle.addEventListener("mouseenter", function () { + navMenu.classList.add("active"); + this.setAttribute("aria-expanded", "true"); + + const spans = this.querySelectorAll("span"); + spans[0].style.transform = "rotate(45deg) translate(7px, 7px)"; + spans[1].style.opacity = "0"; + spans[2].style.transform = "rotate(-45deg) translate(7px, -7px)"; + }); + + // Keep dropdown open when hovering over it + navMenu.addEventListener("mouseenter", function () { + this.classList.add("active"); + }); + + // Close when mouse leaves both hamburger and dropdown + navToggle.addEventListener("mouseleave", function (e) { + // Delay closing to allow moving to dropdown + setTimeout(() => { + if (!navMenu.matches(":hover") && !navToggle.matches(":hover")) { + navMenu.classList.remove("active"); + navToggle.setAttribute("aria-expanded", "false"); + + const spans = navToggle.querySelectorAll("span"); + spans[0].style.transform = "none"; + spans[1].style.opacity = "1"; + spans[2].style.transform = "none"; + } + }, 200); + }); + + navMenu.addEventListener("mouseleave", function () { + setTimeout(() => { + if (!navMenu.matches(":hover") && !navToggle.matches(":hover")) { + navMenu.classList.remove("active"); + navToggle.setAttribute("aria-expanded", "false"); + + const spans = navToggle.querySelectorAll("span"); + spans[0].style.transform = "none"; + spans[1].style.opacity = "1"; + spans[2].style.transform = "none"; + } + }, 200); + }); + + // Click to toggle (for mobile/touch) + navToggle.addEventListener("click", function (e) { + e.stopPropagation(); + const isActive = navMenu.classList.toggle("active"); + this.setAttribute("aria-expanded", isActive ? "true" : "false"); + + // Animate hamburger menu + const spans = this.querySelectorAll("span"); + if (isActive) { + spans[0].style.transform = "rotate(45deg) translate(7px, 7px)"; + spans[1].style.opacity = "0"; + spans[2].style.transform = "rotate(-45deg) translate(7px, -7px)"; + } else { + spans[0].style.transform = "none"; + spans[1].style.opacity = "1"; + spans[2].style.transform = "none"; + } + }); + + // Close dropdown when clicking on a link + const dropdownLinks = navMenu.querySelectorAll("a"); + dropdownLinks.forEach((link) => { + link.addEventListener("click", function () { + navMenu.classList.remove("active"); + navToggle.setAttribute("aria-expanded", "false"); + const spans = navToggle.querySelectorAll("span"); + spans[0].style.transform = "none"; + spans[1].style.opacity = "1"; + spans[2].style.transform = "none"; + }); + }); + + // Close dropdown when clicking outside + document.addEventListener("click", function (event) { + // Don't close if clicking on cart/wishlist dropdowns + if ( + event.target.closest(".dropdown-container") || + event.target.closest(".icon-dropdown") + ) { + return; + } + + const isClickInside = + navToggle.contains(event.target) || navMenu.contains(event.target); + if (!isClickInside && navMenu.classList.contains("active")) { + navMenu.classList.remove("active"); + navToggle.setAttribute("aria-expanded", "false"); + const spans = navToggle.querySelectorAll("span"); + spans[0].style.transform = "none"; + spans[1].style.opacity = "1"; + spans[2].style.transform = "none"; + } + }); + } +}); + +// ==================================== +// Smooth Scrolling for Anchor Links +// ==================================== +document.querySelectorAll('a[href^="#"]').forEach((anchor) => { + anchor.addEventListener("click", function (e) { + const href = this.getAttribute("href"); + if (href !== "#" && href !== "#instagram" && href !== "#wishlist") { + e.preventDefault(); + const target = document.querySelector(href); + if (target) { + target.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + } + } + }); +}); + +// ==================================== +// Shop Page Filtering +// ==================================== +const categoryFilter = document.getElementById("category-filter"); +const sortFilter = document.getElementById("sort-filter"); + +if (categoryFilter) { + categoryFilter.addEventListener("change", function () { + const selectedCategory = this.value; + const productCards = document.querySelectorAll(".product-card"); + + productCards.forEach((card) => { + if (selectedCategory === "all") { + card.style.display = "block"; + } else { + const cardCategory = card.getAttribute("data-category"); + if (cardCategory === selectedCategory) { + card.style.display = "block"; + } else { + card.style.display = "none"; + } + } + }); + }); +} + +if (sortFilter) { + sortFilter.addEventListener("change", function () { + const sortValue = this.value; + const productsGrid = document.querySelector(".products-grid"); + const productCards = Array.from(document.querySelectorAll(".product-card")); + + if (sortValue === "price-low") { + productCards.sort((a, b) => { + const priceA = parseFloat( + a.querySelector(".price").textContent.replace("$", "") + ); + const priceB = parseFloat( + b.querySelector(".price").textContent.replace("$", "") + ); + return priceA - priceB; + }); + } else if (sortValue === "price-high") { + productCards.sort((a, b) => { + const priceA = parseFloat( + a.querySelector(".price").textContent.replace("$", "") + ); + const priceB = parseFloat( + b.querySelector(".price").textContent.replace("$", "") + ); + return priceB - priceA; + }); + } + + // Re-append sorted cards + productCards.forEach((card) => { + productsGrid.appendChild(card); + }); + }); +} + +// ==================================== +// Add to Cart Functionality (Basic) +// ==================================== +document.querySelectorAll(".product-card .btn").forEach((button) => { + button.addEventListener("click", function (e) { + e.preventDefault(); + + // Get product details + const productCard = this.closest(".product-card"); + const productName = productCard.querySelector("h3").textContent; + const productPrice = productCard.querySelector(".price").textContent; + + // Show notification + showNotification(`${productName} added to cart!`); + + // You can expand this to actually store cart items + // For example, using localStorage or sending to a server + }); +}); + +// ==================================== +// Contact Form Handling +// ==================================== +const contactForm = document.getElementById("contactForm"); + +if (contactForm) { + contactForm.addEventListener("submit", function (e) { + e.preventDefault(); + + // Get form values + const name = document.getElementById("name").value; + const email = document.getElementById("email").value; + const phone = document.getElementById("phone").value; + const subject = document.getElementById("subject").value; + const message = document.getElementById("message").value; + + // Basic validation + if (!name || !email || !subject || !message) { + showNotification("Please fill in all required fields!", "error"); + return; + } + + // Email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + showNotification("Please enter a valid email address!", "error"); + return; + } + + // Here you would typically send the form data to a server + // For now, we'll just show a success message + showNotification( + "Thank you! Your message has been sent. We'll get back to you soon.", + "success" + ); + + // Reset form + contactForm.reset(); + }); +} + +// ==================================== +// Notification System +// ==================================== +function showNotification(message, type = "success") { + // Create notification element + const notification = document.createElement("div"); + notification.className = `notification notification-${type}`; + notification.textContent = message; + + // Style the notification + notification.style.cssText = ` + position: fixed; + top: 100px; + right: 20px; + background-color: ${type === "success" ? "#4CAF50" : "#F44336"}; + color: white; + padding: 15px 25px; + border-radius: 5px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + z-index: 10000; + animation: slideIn 0.3s ease-out; + `; + + // Add animation + const style = document.createElement("style"); + style.textContent = ` + @keyframes slideIn { + from { + transform: translateX(400px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + @keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(400px); + opacity: 0; + } + } + `; + document.head.appendChild(style); + + // Add to page + document.body.appendChild(notification); + + // Remove after 3 seconds + setTimeout(() => { + notification.style.animation = "slideOut 0.3s ease-out"; + setTimeout(() => { + notification.remove(); + }, 300); + }, 3000); +} + +// ==================================== +// Image Lazy Loading (Optional Enhancement) +// ==================================== +if ("IntersectionObserver" in window) { + const imageObserver = new IntersectionObserver((entries, observer) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const img = entry.target; + img.src = img.dataset.src || img.src; + img.classList.add("loaded"); + observer.unobserve(img); + } + }); + }); + + document.querySelectorAll("img").forEach((img) => { + imageObserver.observe(img); + }); +} + +// ==================================== +// Scroll to Top Button +// ==================================== +function createScrollToTopButton() { + const button = document.createElement("button"); + button.innerHTML = "↑"; + button.className = "scroll-to-top"; + button.style.cssText = ` + position: fixed; + bottom: 30px; + right: 30px; + width: 50px; + height: 50px; + background-color: #6B4E9B; + color: white; + border: none; + border-radius: 50%; + font-size: 24px; + cursor: pointer; + display: none; + z-index: 1000; + transition: all 0.3s ease; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + `; + + document.body.appendChild(button); + + // Show/hide button based on scroll position + window.addEventListener("scroll", () => { + if (window.pageYOffset > 300) { + button.style.display = "block"; + } else { + button.style.display = "none"; + } + }); + + // Scroll to top when clicked + button.addEventListener("click", () => { + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + }); + + // Hover effect + button.addEventListener("mouseenter", () => { + button.style.backgroundColor = "#5a3e82"; + button.style.transform = "translateY(-3px)"; + }); + + button.addEventListener("mouseleave", () => { + button.style.backgroundColor = "#6B4E9B"; + button.style.transform = "translateY(0)"; + }); +} + +// Initialize scroll to top button +createScrollToTopButton(); + +// ==================================== +// Portfolio Gallery Hover Effects +// ==================================== +document.querySelectorAll(".portfolio-category").forEach((category) => { + category.addEventListener("mouseenter", function () { + this.style.transition = "all 0.3s ease"; + }); +}); + +// ==================================== +// Active Navigation Link Highlighting +// ==================================== +function highlightActiveNavLink() { + const currentPage = window.location.pathname.split("/").pop() || "index.html"; + const navLinks = document.querySelectorAll(".nav-menu a"); + + navLinks.forEach((link) => { + const linkPage = link.getAttribute("href").split("/").pop().split("#")[0]; + if (linkPage === currentPage) { + link.classList.add("active"); + } + }); +} + +highlightActiveNavLink(); + +// ==================================== +// Print console message +// ==================================== +console.log( + "%c Sky Art Shop Website ", + "background: #6B4E9B; color: white; font-size: 20px; padding: 10px;" +); +console.log("Welcome to Sky Art Shop! 🎨");