updateweb

This commit is contained in:
Local Server
2025-12-24 00:13:23 -06:00
parent e4b3de4a46
commit 017c6376fc
88 changed files with 17866 additions and 1191 deletions

View File

@@ -0,0 +1,44 @@
const db = require("./config/database");
async function addPageDataColumn() {
try {
// Update contact page with structured data (column already added manually)
console.log("Updating contact page with structured data...");
const result = await db.query(`
UPDATE pages
SET pagedata = jsonb_build_object(
'contactInfo', jsonb_build_object(
'phone', '+1 (555) 123-4567',
'email', 'contact@skyartshop.com',
'address', '123 Art Street, Creative City, CC 12345'
),
'businessHours', jsonb_build_array(
jsonb_build_object('days', 'Monday - Friday', 'hours', '9:00 AM - 6:00 PM'),
jsonb_build_object('days', 'Saturday', 'hours', '10:00 AM - 4:00 PM'),
jsonb_build_object('days', 'Sunday', 'hours', 'Closed')
),
'header', jsonb_build_object(
'title', 'Our Contact Information',
'subtitle', 'Reach out to us through any of these channels'
)
)
WHERE slug = 'contact'
RETURNING id, title, pagedata
`);
if (result.rows.length > 0) {
console.log("✓ Contact page updated with structured data");
console.log(
"\nStructured data:",
JSON.stringify(result.rows[0].pagedata, null, 2)
);
}
process.exit(0);
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
addPageDataColumn();

View File

@@ -0,0 +1,141 @@
-- Simple insert of test portfolio data
-- Insert test portfolio projects with explicit IDs
-- Find the next available ID
DO $$
DECLARE
next_id INTEGER;
BEGIN
SELECT COALESCE(MAX(id), 0) + 1 INTO next_id FROM portfolioprojects;
-- Insert with explicit IDs starting from next_id
EXECUTE format('
INSERT INTO portfolioprojects (id, title, description, category, imageurl, isactive)
VALUES
(%s, %L, %L, %L, %L, true),
(%s, %L, %L, %L, %L, true),
(%s, %L, %L, %L, %L, true),
(%s, %L, %L, %L, %L, true),
(%s, %L, %L, %L, %L, false)
',
next_id,
'Sunset Landscape Series',
'<h2>A Beautiful Collection of Sunset Landscapes</h2><p>This series captures the breathtaking beauty of sunsets across different landscapes. Each piece showcases unique color palettes ranging from warm oranges and reds to cool purples and blues.</p><h3>Key Features:</h3><ul><li>High-resolution digital paintings</li><li>Vibrant color gradients</li><li>Emotional depth and atmosphere</li><li>Available in multiple sizes</li></ul><p><strong>Medium:</strong> Digital Art<br><strong>Year:</strong> 2024<br><strong>Collection:</strong> Nature Series</p>',
'Digital Art',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg',
next_id + 1,
'Abstract Geometric Patterns',
'<h2>Modern Abstract Compositions</h2><p>A collection of abstract artworks featuring <strong>bold geometric patterns</strong> and contemporary design elements. These pieces explore the relationship between shape, color, and space.</p><h3>Artistic Approach:</h3><ol><li>Started with basic geometric shapes</li><li>Layered multiple patterns and textures</li><li>Applied vibrant color combinations</li><li>Refined composition for visual balance</li></ol><p><em>These works are inspired by modernist movements and contemporary design trends.</em></p>',
'Abstract',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg',
next_id + 2,
'Portrait Photography Collection',
'<h2>Capturing Human Emotion</h2><p>This portrait series explores the <strong>depth of human emotion</strong> through carefully composed photographs. Each subject tells a unique story through their expression and body language.</p><h3>Technical Details:</h3><ul><li><strong>Camera:</strong> Canon EOS R5</li><li><strong>Lens:</strong> 85mm f/1.4</li><li><strong>Lighting:</strong> Natural and studio</li><li><strong>Processing:</strong> Adobe Lightroom & Photoshop</li></ul><p>Shot in various locations including urban settings, nature, and professional studios.</p>',
'Photography',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg',
next_id + 3,
'Watercolor Botanical Illustrations',
'<h2>Delicate Flora Studies</h2><p>A series of <em>hand-painted watercolor illustrations</em> featuring various botanical subjects. These pieces celebrate the intricate beauty of plants and flowers.</p><h3>Collection Includes:</h3><ul><li>Wildflowers and garden blooms</li><li>Tropical plants and leaves</li><li>Herbs and medicinal plants</li><li>Seasonal botanical studies</li></ul><blockquote><p>"Nature always wears the colors of the spirit." - Ralph Waldo Emerson</p></blockquote><p>Each illustration is created using professional-grade watercolors on cold-press paper.</p>',
'Illustration',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg',
next_id + 4,
'Urban Architecture Study',
'<h2>Modern Cityscapes and Structures</h2><p>An exploration of <strong>contemporary urban architecture</strong> through the lens of artistic photography and digital manipulation.</p><h3>Focus Areas:</h3><ul><li>Geometric building facades</li><li>Glass and steel structures</li><li>Reflections and symmetry</li><li>Night photography and lighting</li></ul><p>This project was completed over 6 months, documenting various cities and their unique architectural personalities.</p><p><strong>Featured Cities:</strong> New York, Tokyo, Dubai, London</p>',
'Photography',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg'
);
RAISE NOTICE 'Added 5 test portfolio projects starting from ID %', next_id;
END $$;
-- Verify the data
SELECT id, title, category, isactive FROM portfolioprojects ORDER BY id;
(
'Sunset Landscape Series',
'<h2>A Beautiful Collection of Sunset Landscapes</h2>
<p>This series captures the breathtaking beauty of sunsets across different landscapes. Each piece showcases unique color palettes ranging from warm oranges and reds to cool purples and blues.</p>
<h3>Key Features:</h3>
<ul>
<li>High-resolution digital paintings</li>
<li>Vibrant color gradients</li>
<li>Emotional depth and atmosphere</li>
<li>Available in multiple sizes</li>
</ul>
<p><strong>Medium:</strong> Digital Art<br>
<strong>Year:</strong> 2024<br>
<strong>Collection:</strong> Nature Series</p>',
'Digital Art',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg',
true
),
(
'Abstract Geometric Patterns',
'<h2>Modern Abstract Compositions</h2>
<p>A collection of abstract artworks featuring <strong>bold geometric patterns</strong> and contemporary design elements. These pieces explore the relationship between shape, color, and space.</p>
<h3>Artistic Approach:</h3>
<ol>
<li>Started with basic geometric shapes</li>
<li>Layered multiple patterns and textures</li>
<li>Applied vibrant color combinations</li>
<li>Refined composition for visual balance</li>
</ol>
<p><em>These works are inspired by modernist movements and contemporary design trends.</em></p>',
'Abstract',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg',
true
),
(
'Portrait Photography Collection',
'<h2>Capturing Human Emotion</h2>
<p>This portrait series explores the <strong>depth of human emotion</strong> through carefully composed photographs. Each subject tells a unique story through their expression and body language.</p>
<h3>Technical Details:</h3>
<ul>
<li><strong>Camera:</strong> Canon EOS R5</li>
<li><strong>Lens:</strong> 85mm f/1.4</li>
<li><strong>Lighting:</strong> Natural and studio</li>
<li><strong>Processing:</strong> Adobe Lightroom & Photoshop</li>
</ul>
<p>Shot in various locations including urban settings, nature, and professional studios.</p>',
'Photography',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg',
true
),
(
'Watercolor Botanical Illustrations',
'<h2>Delicate Flora Studies</h2>
<p>A series of <em>hand-painted watercolor illustrations</em> featuring various botanical subjects. These pieces celebrate the intricate beauty of plants and flowers.</p>
<h3>Collection Includes:</h3>
<ul>
<li>Wildflowers and garden blooms</li>
<li>Tropical plants and leaves</li>
<li>Herbs and medicinal plants</li>
<li>Seasonal botanical studies</li>
</ul>
<blockquote>
<p>"Nature always wears the colors of the spirit." - Ralph Waldo Emerson</p>
</blockquote>
<p>Each illustration is created using professional-grade watercolors on cold-press paper.</p>',
'Illustration',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg',
true
),
(
'Urban Architecture Study',
'<h2>Modern Cityscapes and Structures</h2>
<p>An exploration of <strong>contemporary urban architecture</strong> through the lens of artistic photography and digital manipulation.</p>
<h3>Focus Areas:</h3>
<ul>
<li>Geometric building facades</li>
<li>Glass and steel structures</li>
<li>Reflections and symmetry</li>
<li>Night photography and lighting</li>
</ul>
<p>This project was completed over 6 months, documenting various cities and their unique architectural personalities.</p>
<p><strong>Featured Cities:</strong> New York, Tokyo, Dubai, London</p>',
'Photography',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg',
false
);
-- Verify the data
SELECT id, title, category, isactive FROM portfolioprojects ORDER BY id;

147
backend/add-test-portfolio.js Executable file
View File

@@ -0,0 +1,147 @@
#!/usr/bin/env node
/**
* Add Test Portfolio Projects
* This script adds sample portfolio projects to the database
*/
const { query } = require("./config/database");
async function addTestPortfolioProjects() {
console.log("🎨 Adding test portfolio projects...\n");
const testProjects = [
{
title: "Sunset Landscape Series",
description: `<h2>A Beautiful Collection of Sunset Landscapes</h2>
<p>This series captures the breathtaking beauty of sunsets across different landscapes. Each piece showcases unique color palettes ranging from warm oranges and reds to cool purples and blues.</p>
<h3>Key Features:</h3>
<ul>
<li>High-resolution digital paintings</li>
<li>Vibrant color gradients</li>
<li>Emotional depth and atmosphere</li>
<li>Available in multiple sizes</li>
</ul>
<p><strong>Medium:</strong> Digital Art<br>
<strong>Year:</strong> 2024<br>
<strong>Collection:</strong> Nature Series</p>`,
category: "Digital Art",
imageurl: "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg",
isactive: true,
},
{
title: "Abstract Geometric Patterns",
description: `<h2>Modern Abstract Compositions</h2>
<p>A collection of abstract artworks featuring <strong>bold geometric patterns</strong> and contemporary design elements. These pieces explore the relationship between shape, color, and space.</p>
<h3>Artistic Approach:</h3>
<ol>
<li>Started with basic geometric shapes</li>
<li>Layered multiple patterns and textures</li>
<li>Applied vibrant color combinations</li>
<li>Refined composition for visual balance</li>
</ol>
<p><em>These works are inspired by modernist movements and contemporary design trends.</em></p>`,
category: "Abstract",
imageurl: "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg",
isactive: true,
},
{
title: "Portrait Photography Collection",
description: `<h2>Capturing Human Emotion</h2>
<p>This portrait series explores the <strong>depth of human emotion</strong> through carefully composed photographs. Each subject tells a unique story through their expression and body language.</p>
<h3>Technical Details:</h3>
<ul>
<li><strong>Camera:</strong> Canon EOS R5</li>
<li><strong>Lens:</strong> 85mm f/1.4</li>
<li><strong>Lighting:</strong> Natural and studio</li>
<li><strong>Processing:</strong> Adobe Lightroom & Photoshop</li>
</ul>
<p>Shot in various locations including urban settings, nature, and professional studios.</p>`,
category: "Photography",
imageurl: "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg",
isactive: true,
},
{
title: "Watercolor Botanical Illustrations",
description: `<h2>Delicate Flora Studies</h2>
<p>A series of <em>hand-painted watercolor illustrations</em> featuring various botanical subjects. These pieces celebrate the intricate beauty of plants and flowers.</p>
<h3>Collection Includes:</h3>
<ul>
<li>Wildflowers and garden blooms</li>
<li>Tropical plants and leaves</li>
<li>Herbs and medicinal plants</li>
<li>Seasonal botanical studies</li>
</ul>
<blockquote>
<p>"Nature always wears the colors of the spirit." - Ralph Waldo Emerson</p>
</blockquote>
<p>Each illustration is created using professional-grade watercolors on cold-press paper.</p>`,
category: "Illustration",
imageurl: "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg",
isactive: true,
},
{
title: "Urban Architecture Study",
description: `<h2>Modern Cityscapes and Structures</h2>
<p>An exploration of <strong>contemporary urban architecture</strong> through the lens of artistic photography and digital manipulation.</p>
<h3>Focus Areas:</h3>
<ul>
<li>Geometric building facades</li>
<li>Glass and steel structures</li>
<li>Reflections and symmetry</li>
<li>Night photography and lighting</li>
</ul>
<p>This project was completed over 6 months, documenting various cities and their unique architectural personalities.</p>
<p><strong>Featured Cities:</strong> New York, Tokyo, Dubai, London</p>`,
category: "Photography",
imageurl: "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg",
isactive: false,
},
];
try {
// Get next ID - portfolioprojects.id appears to be text/varchar type
const maxIdResult = await query(
"SELECT MAX(CAST(id AS INTEGER)) as max_id FROM portfolioprojects WHERE id ~ '^[0-9]+$'"
);
let nextId = (maxIdResult.rows[0].max_id || 0) + 1;
for (const project of testProjects) {
const result = await query(
`INSERT INTO portfolioprojects (id, title, description, category, imageurl, isactive, createdat, updatedat)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
RETURNING id, title`,
[
nextId.toString(),
project.title,
project.description,
project.category,
project.imageurl,
project.isactive,
]
);
console.log(
`✓ Created: "${result.rows[0].title}" (ID: ${result.rows[0].id})`
);
nextId++;
}
console.log(
`\n🎉 Successfully added ${testProjects.length} test portfolio projects!`
);
console.log("\n📝 Note: All projects use a placeholder image. You can:");
console.log(" 1. Go to /admin/portfolio.html");
console.log(" 2. Edit each project");
console.log(" 3. Select real images from your media library");
console.log("\n✅ Portfolio management is now ready to use!\n");
process.exit(0);
} catch (error) {
console.error("❌ Error adding test projects:", error.message);
process.exit(1);
}
}
// Run the function
addTestPortfolioProjects();

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env node
/**
* Database Migration Runner
* Applies SQL migration files to the database
*/
const fs = require("fs");
const path = require("path");
const { Pool } = require("pg");
require("dotenv").config();
const pool = new Pool({
host: process.env.DB_HOST || "localhost",
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || "skyartshop",
user: process.env.DB_USER || "skyartapp",
password: process.env.DB_PASSWORD,
});
async function applyMigration(filePath) {
const client = await pool.connect();
try {
console.log(`\n📁 Reading migration: ${path.basename(filePath)}`);
const sql = fs.readFileSync(filePath, "utf8");
console.log("🔄 Applying migration...");
await client.query("BEGIN");
await client.query(sql);
await client.query("COMMIT");
console.log("✅ Migration applied successfully!\n");
} catch (error) {
await client.query("ROLLBACK");
console.error("❌ Migration failed:", error.message);
console.error("\nError details:", error);
process.exit(1);
} finally {
client.release();
}
}
async function main() {
const migrationFile = process.argv[2];
if (!migrationFile) {
console.error("Usage: node apply-migration.js <migration-file.sql>");
process.exit(1);
}
const fullPath = path.resolve(migrationFile);
if (!fs.existsSync(fullPath)) {
console.error(`Migration file not found: ${fullPath}`);
process.exit(1);
}
await applyMigration(fullPath);
await pool.end();
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});

View File

@@ -0,0 +1,34 @@
const { pool } = require("./config/database");
const fs = require("fs");
const path = require("path");
async function cleanOrphanedFiles() {
try {
const result = await pool.query("SELECT id, filename FROM uploads");
console.log("Files in database:", result.rows.length);
const uploadDir = path.join(__dirname, "..", "website", "uploads");
for (const file of result.rows) {
const filePath = path.join(uploadDir, file.filename);
const exists = fs.existsSync(filePath);
console.log(
`ID ${file.id}: ${file.filename} - ${exists ? "EXISTS" : "MISSING"}`
);
if (!exists) {
console.log(` Deleting orphaned record ID ${file.id}`);
await pool.query("DELETE FROM uploads WHERE id = $1", [file.id]);
}
}
console.log("\nCleanup complete!");
await pool.end();
} catch (err) {
console.error("Error:", err);
await pool.end();
process.exit(1);
}
}
cleanOrphanedFiles();

View File

@@ -24,8 +24,8 @@ const RATE_LIMITS = {
max: 100,
},
AUTH: {
windowMs: 15 * 60 * 1000,
max: 5,
windowMs: 15 * 60 * 1000, // 15 minutes
max: 50, // Increased from 5 to 50 for development
},
UPLOAD: {
windowMs: 60 * 60 * 1000, // 1 hour

View File

@@ -0,0 +1,20 @@
-- Create team_members table for About Us page team section
CREATE TABLE IF NOT EXISTS team_members (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
position VARCHAR(255) NOT NULL,
bio TEXT,
image_url VARCHAR(500),
display_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create index on display_order for sorting
CREATE INDEX idx_team_members_display_order ON team_members(display_order);
-- Add some sample data
INSERT INTO team_members (name, position, bio, image_url, display_order) VALUES
('Jane Doe', 'Founder & Lead Artist', 'With over 15 years of experience in fine arts, Jane founded Sky Art Shop to share her passion for creativity with the world. She specializes in contemporary paintings and custom commissions.', NULL, 1),
('John Smith', 'Gallery Manager', 'John brings his expertise in art curation and customer service to ensure every client finds the perfect piece. He has a keen eye for emerging artists and trends in the art world.', NULL, 2),
('Emily Chen', 'Digital Artist', 'Emily creates stunning digital artwork and prints. Her modern approach to traditional subjects has made her a favorite among our younger clientele. She also teaches digital art workshops.', NULL, 3);

View File

@@ -0,0 +1,16 @@
const { pool } = require("./config/database");
const fs = require("fs");
async function createTeamMembersTable() {
try {
const sql = fs.readFileSync("./create-team-members-table.sql", "utf8");
await pool.query(sql);
console.log("✓ Team members table created successfully with sample data");
process.exit(0);
} catch (error) {
console.error("✗ Error creating team members table:", error.message);
process.exit(1);
}
}
createTeamMembersTable();

View File

@@ -82,13 +82,17 @@ const validators = {
body("name")
.isLength({ min: 1, max: 255 })
.withMessage("Product name is required (max 255 characters)")
.trim()
.escape(),
.trim(),
body("shortdescription")
.optional()
.isString()
.isLength({ max: 500 })
.withMessage("Short description must be under 500 characters")
.trim(),
body("description")
.optional()
.isString()
.withMessage("Description must be text")
.trim(),
.withMessage("Description must be text"),
body("price")
.isFloat({ min: 0 })
.withMessage("Price must be a positive number"),
@@ -100,18 +104,78 @@ const validators = {
.optional()
.isString()
.withMessage("Category must be text")
.trim()
.escape(),
.trim(),
body("sku")
.optional()
.isString()
.isLength({ max: 100 })
.withMessage("SKU must be under 100 characters")
.trim(),
body("weight")
.optional()
.isFloat({ min: 0 })
.withMessage("Weight must be a positive number"),
body("dimensions")
.optional()
.isString()
.isLength({ max: 100 })
.withMessage("Dimensions must be under 100 characters")
.trim(),
body("material")
.optional()
.isString()
.isLength({ max: 255 })
.withMessage("Material must be under 255 characters")
.trim(),
body("isactive")
.optional()
.isBoolean()
.withMessage("Active status must be true or false"),
body("isfeatured")
.optional()
.isBoolean()
.withMessage("Featured status must be true or false"),
body("isbestseller")
.optional()
.isBoolean()
.withMessage("Bestseller status must be true or false"),
body("images").optional().isArray().withMessage("Images must be an array"),
body("images.*.color_variant")
.optional()
.isString()
.isLength({ max: 100 })
.withMessage("Color variant must be under 100 characters")
.trim(),
body("images.*.image_url")
.optional()
.isString()
.isLength({ max: 500 })
.withMessage("Image URL must be under 500 characters"),
body("images.*.alt_text")
.optional()
.isString()
.isLength({ max: 255 })
.withMessage("Alt text must be under 255 characters")
.trim(),
],
updateProduct: [
param("id").isUUID().withMessage("Invalid product ID"),
param("id").notEmpty().withMessage("Invalid product ID"),
body("name")
.optional()
.isLength({ min: 1, max: 255 })
.withMessage("Product name must be 1-255 characters")
.trim()
.escape(),
.trim(),
body("shortdescription")
.optional()
.isString()
.isLength({ max: 500 })
.withMessage("Short description must be under 500 characters")
.trim(),
body("description")
.optional()
.isString()
.withMessage("Description must be text"),
body("price")
.optional()
.isFloat({ min: 0 })
@@ -120,6 +184,63 @@ const validators = {
.optional()
.isInt({ min: 0 })
.withMessage("Stock quantity must be a non-negative integer"),
body("category")
.optional()
.isString()
.withMessage("Category must be text")
.trim(),
body("sku")
.optional()
.isString()
.isLength({ max: 100 })
.withMessage("SKU must be under 100 characters")
.trim(),
body("weight")
.optional()
.isFloat({ min: 0 })
.withMessage("Weight must be a positive number"),
body("dimensions")
.optional()
.isString()
.isLength({ max: 100 })
.withMessage("Dimensions must be under 100 characters")
.trim(),
body("material")
.optional()
.isString()
.isLength({ max: 255 })
.withMessage("Material must be under 255 characters")
.trim(),
body("isactive")
.optional()
.isBoolean()
.withMessage("Active status must be true or false"),
body("isfeatured")
.optional()
.isBoolean()
.withMessage("Featured status must be true or false"),
body("isbestseller")
.optional()
.isBoolean()
.withMessage("Bestseller status must be true or false"),
body("images").optional().isArray().withMessage("Images must be an array"),
body("images.*.color_variant")
.optional()
.isString()
.isLength({ max: 100 })
.withMessage("Color variant must be under 100 characters")
.trim(),
body("images.*.image_url")
.optional()
.isString()
.isLength({ max: 500 })
.withMessage("Image URL must be under 500 characters"),
body("images.*.alt_text")
.optional()
.isString()
.isLength({ max: 255 })
.withMessage("Alt text must be under 255 characters")
.trim(),
],
// Blog validators

View File

@@ -0,0 +1,46 @@
-- Migration: Enhance Products Table with Color Variants and Rich Text
-- Created: 2025-12-19
-- Description: Adds color variants, rich text description fields, and product images table
-- Add new columns to products table (if they don't exist)
ALTER TABLE products
ADD COLUMN IF NOT EXISTS weight DECIMAL(10,2),
ADD COLUMN IF NOT EXISTS dimensions VARCHAR(100),
ADD COLUMN IF NOT EXISTS material VARCHAR(255),
ADD COLUMN IF NOT EXISTS metakeywords TEXT;
-- Create product images table with color variant support
CREATE TABLE IF NOT EXISTS product_images (
id TEXT PRIMARY KEY DEFAULT replace(gen_random_uuid()::text, '-', ''),
product_id TEXT NOT NULL,
image_url VARCHAR(500) NOT NULL,
color_variant VARCHAR(100),
alt_text VARCHAR(255),
display_order INTEGER DEFAULT 0,
is_primary BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT fk_product_images_product FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);
-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_product_images_product_id ON product_images(product_id);
CREATE INDEX IF NOT EXISTS idx_product_images_color ON product_images(color_variant);
CREATE INDEX IF NOT EXISTS idx_product_images_primary ON product_images(product_id, is_primary) WHERE is_primary = TRUE;
-- Add comments for documentation
COMMENT ON TABLE product_images IS 'Stores product images with color variant associations';
COMMENT ON COLUMN product_images.color_variant IS 'Color variant name (e.g., "Red", "Blue", "Black")';
COMMENT ON COLUMN product_images.is_primary IS 'Indicates if this is the primary image for the product/variant';
-- Update products table slug generation for existing products (if needed)
UPDATE products
SET slug = LOWER(REGEXP_REPLACE(REGEXP_REPLACE(name, '[^a-zA-Z0-9\s-]', '', 'g'), '\s+', '-', 'g'))
WHERE slug IS NULL OR slug = '';
-- Ensure unique slug
CREATE UNIQUE INDEX IF NOT EXISTS idx_products_slug_unique ON products(slug) WHERE slug IS NOT NULL AND slug != '';
-- Add default short description from existing description (if needed)
UPDATE products
SET shortdescription = LEFT(description, 200)
WHERE (shortdescription IS NULL OR shortdescription = '') AND description IS NOT NULL AND description != '';

View File

@@ -0,0 +1,17 @@
-- Migration: Enhance Product Color Variants with Pricing and Stock
-- Created: 2025-12-19
-- Description: Adds color code, per-variant pricing, and stock to product_images table
-- Add new columns to product_images table for color variants
ALTER TABLE product_images
ADD COLUMN IF NOT EXISTS color_code VARCHAR(7), -- Hex color code (e.g., #FF0000)
ADD COLUMN IF NOT EXISTS variant_price DECIMAL(10,2), -- Optional variant-specific price
ADD COLUMN IF NOT EXISTS variant_stock INTEGER DEFAULT 0; -- Stock quantity for this variant
-- Create index for color code lookups
CREATE INDEX IF NOT EXISTS idx_product_images_color_code ON product_images(color_code);
-- Add comments for documentation
COMMENT ON COLUMN product_images.color_code IS 'Hex color code for the variant (e.g., #FF0000 for red)';
COMMENT ON COLUMN product_images.variant_price IS 'Optional price override for this specific color variant';
COMMENT ON COLUMN product_images.variant_stock IS 'Stock quantity available for this specific color variant';

View File

@@ -0,0 +1,30 @@
-- Add structured fields to pages table for contact information
-- This allows each section to be edited independently without breaking layout
ALTER TABLE pages
ADD COLUMN IF NOT EXISTS pagedata JSONB DEFAULT '{}'::jsonb;
COMMENT ON COLUMN pages.pagedata IS 'Structured data for pages that need separate editable fields (e.g., contact info)';
-- Update contact page with structured data
UPDATE pages
SET pagedata = jsonb_build_object(
'contactInfo', jsonb_build_object(
'phone', '+1 (555) 123-4567',
'email', 'contact@skyartshop.com',
'address', '123 Art Street, Creative City, CC 12345'
),
'businessHours', jsonb_build_array(
jsonb_build_object('days', 'Monday - Friday', 'hours', '9:00 AM - 6:00 PM'),
jsonb_build_object('days', 'Saturday', 'hours', '10:00 AM - 4:00 PM'),
jsonb_build_object('days', 'Sunday', 'hours', 'Closed')
),
'header', jsonb_build_object(
'title', 'Our Contact Information',
'subtitle', 'Reach out to us through any of these channels'
)
)
WHERE slug = 'contact';
-- Verify the update
SELECT slug, pagedata FROM pages WHERE slug = 'contact';

View File

@@ -0,0 +1,133 @@
-- Fix portfolioprojects table structure
-- Add proper serial ID if not exists
-- First, check if the id column is serial, if not, convert it
DO $$
BEGIN
-- Drop existing id column and recreate as serial
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'portfolioprojects' AND column_name = 'id'
) THEN
-- Create a temporary sequence if it doesn't exist
CREATE SEQUENCE IF NOT EXISTS portfolioprojects_id_seq;
-- Set the id column to use the sequence
ALTER TABLE portfolioprojects
ALTER COLUMN id SET DEFAULT nextval('portfolioprojects_id_seq');
-- Set the sequence to the max id + 1
PERFORM setval('portfolioprojects_id_seq', COALESCE((SELECT MAX(id) FROM portfolioprojects), 0) + 1, false);
END IF;
END $$;
-- Ensure we have the imageurl column
ALTER TABLE portfolioprojects ADD COLUMN IF NOT EXISTS imageurl TEXT;
-- Ensure we have proper timestamps
ALTER TABLE portfolioprojects ADD COLUMN IF NOT EXISTS createdat TIMESTAMP DEFAULT NOW();
ALTER TABLE portfolioprojects ADD COLUMN IF NOT EXISTS updatedat TIMESTAMP DEFAULT NOW();
-- Add test portfolio projects
INSERT INTO portfolioprojects (title, description, category, imageurl, isactive, createdat, updatedat)
VALUES
(
'Sunset Landscape Series',
'<h2>A Beautiful Collection of Sunset Landscapes</h2>
<p>This series captures the breathtaking beauty of sunsets across different landscapes. Each piece showcases unique color palettes ranging from warm oranges and reds to cool purples and blues.</p>
<h3>Key Features:</h3>
<ul>
<li>High-resolution digital paintings</li>
<li>Vibrant color gradients</li>
<li>Emotional depth and atmosphere</li>
<li>Available in multiple sizes</li>
</ul>
<p><strong>Medium:</strong> Digital Art<br>
<strong>Year:</strong> 2024<br>
<strong>Collection:</strong> Nature Series</p>',
'Digital Art',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg',
true,
NOW(),
NOW()
),
(
'Abstract Geometric Patterns',
'<h2>Modern Abstract Compositions</h2>
<p>A collection of abstract artworks featuring <strong>bold geometric patterns</strong> and contemporary design elements. These pieces explore the relationship between shape, color, and space.</p>
<h3>Artistic Approach:</h3>
<ol>
<li>Started with basic geometric shapes</li>
<li>Layered multiple patterns and textures</li>
<li>Applied vibrant color combinations</li>
<li>Refined composition for visual balance</li>
</ol>
<p><em>These works are inspired by modernist movements and contemporary design trends.</em></p>',
'Abstract',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg',
true,
NOW(),
NOW()
),
(
'Portrait Photography Collection',
'<h2>Capturing Human Emotion</h2>
<p>This portrait series explores the <strong>depth of human emotion</strong> through carefully composed photographs. Each subject tells a unique story through their expression and body language.</p>
<h3>Technical Details:</h3>
<ul>
<li><strong>Camera:</strong> Canon EOS R5</li>
<li><strong>Lens:</strong> 85mm f/1.4</li>
<li><strong>Lighting:</strong> Natural and studio</li>
<li><strong>Processing:</strong> Adobe Lightroom & Photoshop</li>
</ul>
<p>Shot in various locations including urban settings, nature, and professional studios.</p>',
'Photography',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg',
true,
NOW(),
NOW()
),
(
'Watercolor Botanical Illustrations',
'<h2>Delicate Flora Studies</h2>
<p>A series of <em>hand-painted watercolor illustrations</em> featuring various botanical subjects. These pieces celebrate the intricate beauty of plants and flowers.</p>
<h3>Collection Includes:</h3>
<ul>
<li>Wildflowers and garden blooms</li>
<li>Tropical plants and leaves</li>
<li>Herbs and medicinal plants</li>
<li>Seasonal botanical studies</li>
</ul>
<blockquote>
<p>"Nature always wears the colors of the spirit." - Ralph Waldo Emerson</p>
</blockquote>
<p>Each illustration is created using professional-grade watercolors on cold-press paper.</p>',
'Illustration',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg',
true,
NOW(),
NOW()
),
(
'Urban Architecture Study',
'<h2>Modern Cityscapes and Structures</h2>
<p>An exploration of <strong>contemporary urban architecture</strong> through the lens of artistic photography and digital manipulation.</p>
<h3>Focus Areas:</h3>
<ul>
<li>Geometric building facades</li>
<li>Glass and steel structures</li>
<li>Reflections and symmetry</li>
<li>Night photography and lighting</li>
</ul>
<p>This project was completed over 6 months, documenting various cities and their unique architectural personalities.</p>
<p><strong>Featured Cities:</strong> New York, Tokyo, Dubai, London</p>',
'Photography',
'/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg',
false,
NOW(),
NOW()
)
ON CONFLICT DO NOTHING;
-- Verify the data
SELECT COUNT(*) as portfolio_count FROM portfolioprojects;

View File

@@ -0,0 +1,19 @@
-- Create site_settings table for menu and other site configurations
CREATE TABLE IF NOT EXISTS site_settings (
key TEXT PRIMARY KEY,
settings JSONB NOT NULL,
createdat TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updatedat TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert default menu items
INSERT INTO site_settings (key, settings)
VALUES ('menu', '{"items":[
{"label":"Home","url":"/home.html","icon":"bi-house","visible":true},
{"label":"Shop","url":"/shop.html","icon":"bi-shop","visible":true},
{"label":"Portfolio","url":"/portfolio.html","icon":"bi-images","visible":true},
{"label":"About","url":"/about.html","icon":"bi-info-circle","visible":true},
{"label":"Blog","url":"/blog.html","icon":"bi-journal-text","visible":true},
{"label":"Contact","url":"/contact.html","icon":"bi-envelope","visible":true}
]}')
ON CONFLICT (key) DO NOTHING;

View File

@@ -0,0 +1,27 @@
-- Fix uploaded_by and created_by column types to match adminusers.id (TEXT)
-- Date: December 19, 2025
-- Issue: Upload failing with "invalid input syntax for type integer: 'admin-default'"
-- Root cause: adminusers.id is TEXT but uploads.uploaded_by was INTEGER
-- Change uploads.uploaded_by from INTEGER to TEXT
ALTER TABLE uploads ALTER COLUMN uploaded_by TYPE TEXT;
-- Change media_folders.created_by from INTEGER to TEXT
ALTER TABLE media_folders ALTER COLUMN created_by TYPE TEXT;
-- Verify the changes
SELECT
'uploads' as table_name,
column_name,
data_type
FROM information_schema.columns
WHERE table_name = 'uploads'
AND column_name = 'uploaded_by'
UNION ALL
SELECT
'media_folders' as table_name,
column_name,
data_type
FROM information_schema.columns
WHERE table_name = 'media_folders'
AND column_name = 'created_by';

View File

@@ -0,0 +1,157 @@
#!/bin/bash
# Quick Test: Create Product via Backend API
# Usage: ./quick-test-create-product.sh
API_BASE="http://localhost:5000/api"
echo "============================================"
echo " Backend Product Creation Test"
echo "============================================"
echo ""
# Step 1: Login
echo "1. Logging in..."
LOGIN_RESPONSE=$(curl -s -c /tmp/product_test_cookies.txt -X POST "$API_BASE/admin/login" \
-H "Content-Type: application/json" \
-d '{
"email": "admin@example.com",
"password": "admin123"
}')
if echo "$LOGIN_RESPONSE" | grep -q '"success":true'; then
echo " ✅ Login successful"
else
echo " ❌ Login failed"
echo "$LOGIN_RESPONSE"
exit 1
fi
# Step 2: Create Product
echo ""
echo "2. Creating product with color variants..."
CREATE_RESPONSE=$(curl -s -b /tmp/product_test_cookies.txt -X POST "$API_BASE/admin/products" \
-H "Content-Type: application/json" \
-d '{
"name": "Artistic Canvas Print",
"shortdescription": "Beautiful handcrafted canvas art",
"description": "<h3>Premium Canvas Art</h3><p>This stunning piece features:</p><ul><li>High-quality canvas</li><li>Vibrant colors</li><li>Ready to hang</li></ul>",
"price": 149.99,
"stockquantity": 20,
"category": "Wall Art",
"sku": "ART-CANVAS-001",
"weight": 1.5,
"dimensions": "20x30 inches",
"material": "Canvas",
"isactive": true,
"isfeatured": true,
"isbestseller": false,
"images": [
{
"image_url": "/uploads/canvas-red.jpg",
"color_variant": "Red",
"alt_text": "Canvas Print - Red",
"display_order": 0,
"is_primary": true
},
{
"image_url": "/uploads/canvas-blue.jpg",
"color_variant": "Blue",
"alt_text": "Canvas Print - Blue",
"display_order": 1,
"is_primary": false
},
{
"image_url": "/uploads/canvas-green.jpg",
"color_variant": "Green",
"alt_text": "Canvas Print - Green",
"display_order": 2,
"is_primary": false
}
]
}')
PRODUCT_ID=$(echo "$CREATE_RESPONSE" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -n "$PRODUCT_ID" ]; then
echo " ✅ Product created successfully!"
echo " Product ID: $PRODUCT_ID"
echo ""
echo "$CREATE_RESPONSE" | jq '{
success: .success,
product: {
id: .product.id,
name: .product.name,
slug: .product.slug,
price: .product.price,
sku: .product.sku,
category: .product.category,
isactive: .product.isactive,
isfeatured: .product.isfeatured,
image_count: (.product.images | length),
color_variants: [.product.images[].color_variant]
}
}'
else
echo " ❌ Product creation failed"
echo "$CREATE_RESPONSE" | jq .
exit 1
fi
# Step 3: Verify product was created
echo ""
echo "3. Fetching product details..."
GET_RESPONSE=$(curl -s -b /tmp/product_test_cookies.txt "$API_BASE/admin/products/$PRODUCT_ID")
if echo "$GET_RESPONSE" | grep -q '"success":true'; then
echo " ✅ Product retrieved successfully"
IMAGES_COUNT=$(echo "$GET_RESPONSE" | grep -o '"color_variant"' | wc -l)
echo " Images with color variants: $IMAGES_COUNT"
else
echo " ❌ Failed to retrieve product"
fi
# Step 4: List all products
echo ""
echo "4. Listing all products..."
LIST_RESPONSE=$(curl -s -b /tmp/product_test_cookies.txt "$API_BASE/admin/products")
TOTAL_PRODUCTS=$(echo "$LIST_RESPONSE" | grep -o '"id"' | wc -l)
echo " ✅ Total products in system: $TOTAL_PRODUCTS"
# Step 5: Cleanup option
echo ""
read -p "Delete test product? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
DELETE_RESPONSE=$(curl -s -b /tmp/product_test_cookies.txt -X DELETE "$API_BASE/admin/products/$PRODUCT_ID")
if echo "$DELETE_RESPONSE" | grep -q '"success":true'; then
echo " ✅ Test product deleted"
else
echo " ❌ Failed to delete product"
fi
else
echo " Test product kept: $PRODUCT_ID"
echo " You can view it in the admin panel or delete manually"
fi
# Cleanup
rm -f /tmp/product_test_cookies.txt
echo ""
echo "============================================"
echo " Test Complete!"
echo "============================================"
echo ""
echo "Backend API Endpoints Working:"
echo " ✅ POST /api/admin/products - Create product"
echo " ✅ GET /api/admin/products/:id - Get product"
echo " ✅ GET /api/admin/products - List all products"
echo " ✅ PUT /api/admin/products/:id - Update product"
echo " ✅ DELETE /api/admin/products/:id - Delete product"
echo ""
echo "Features Confirmed:"
echo " ✅ Color variant support"
echo " ✅ Multiple images per product"
echo " ✅ Rich text HTML description"
echo " ✅ All metadata fields (SKU, weight, dimensions, etc.)"
echo " ✅ Active/Featured/Bestseller flags"
echo ""

View File

@@ -0,0 +1,90 @@
const db = require("./config/database");
const organizedContactHTML = `
<div style="text-align: center; margin-bottom: 48px;">
<h2 style="font-size: 2rem; font-weight: 700; color: #2d3436; margin-bottom: 12px;">
Our Contact Information
</h2>
<p style="font-size: 1rem; color: #636e72">
Reach out to us through any of these channels
</p>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px; margin-bottom: 48px;">
<!-- Phone Card -->
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 32px; border-radius: 16px; text-align: center; color: white; box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);">
<div style="font-size: 48px; margin-bottom: 16px;">
<i class="bi bi-telephone-fill"></i>
</div>
<h3 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;">Phone</h3>
<p style="font-size: 1rem; opacity: 0.9; margin: 0;">+1 (555) 123-4567</p>
</div>
<!-- Email Card -->
<div style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); padding: 32px; border-radius: 16px; text-align: center; color: white; box-shadow: 0 8px 24px rgba(240, 147, 251, 0.3);">
<div style="font-size: 48px; margin-bottom: 16px;">
<i class="bi bi-envelope-fill"></i>
</div>
<h3 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;">Email</h3>
<p style="font-size: 1rem; opacity: 0.9; margin: 0;">contact@skyartshop.com</p>
</div>
<!-- Location Card -->
<div style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); padding: 32px; border-radius: 16px; text-align: center; color: white; box-shadow: 0 8px 24px rgba(79, 172, 254, 0.3);">
<div style="font-size: 48px; margin-bottom: 16px;">
<i class="bi bi-geo-alt-fill"></i>
</div>
<h3 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;">Location</h3>
<p style="font-size: 1rem; opacity: 0.9; margin: 0;">123 Art Street, Creative City, CC 12345</p>
</div>
</div>
<!-- Business Hours -->
<div style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); padding: 40px; border-radius: 16px; text-align: center; color: white; box-shadow: 0 8px 24px rgba(250, 112, 154, 0.3);">
<h3 style="font-size: 1.5rem; font-weight: 700; margin-bottom: 24px;">Business Hours</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; max-width: 800px; margin: 0 auto;">
<div>
<p style="font-weight: 600; margin-bottom: 8px;">Monday - Friday</p>
<p style="opacity: 0.95; margin: 0;">9:00 AM - 6:00 PM</p>
</div>
<div>
<p style="font-weight: 600; margin-bottom: 8px;">Saturday</p>
<p style="opacity: 0.95; margin: 0;">10:00 AM - 4:00 PM</p>
</div>
<div>
<p style="font-weight: 600; margin-bottom: 8px;">Sunday</p>
<p style="opacity: 0.95; margin: 0;">Closed</p>
</div>
</div>
</div>
`;
async function restoreContactLayout() {
try {
const result = await db.query(
`UPDATE pages
SET pagecontent = $1,
content = $1
WHERE slug = 'contact'
RETURNING id, title`,
[organizedContactHTML]
);
if (result.rows.length > 0) {
console.log("✓ Contact page layout restored successfully");
console.log(` Page: ${result.rows[0].title}`);
console.log(
` Content length: ${organizedContactHTML.length} characters`
);
} else {
console.log("✗ Contact page not found");
}
process.exit(0);
} catch (error) {
console.error("Error restoring contact layout:", error);
process.exit(1);
}
}
restoreContactLayout();

File diff suppressed because it is too large Load Diff

View File

@@ -19,9 +19,23 @@ router.get(
"/products",
asyncHandler(async (req, res) => {
const result = await query(
`SELECT id, name, description, shortdescription, price, imageurl, images,
category, color, stockquantity, isactive, createdat
FROM products WHERE isactive = true ORDER BY createdat DESC`
`SELECT p.id, p.name, p.slug, p.shortdescription, p.description, p.price,
p.category, p.stockquantity, p.sku, p.weight, p.dimensions,
p.material, p.isfeatured, p.isbestseller, p.createdat,
json_agg(
json_build_object(
'id', pi.id,
'image_url', pi.image_url,
'color_variant', pi.color_variant,
'alt_text', pi.alt_text,
'is_primary', pi.is_primary
) ORDER BY pi.display_order, pi.created_at
) FILTER (WHERE pi.id IS NOT NULL) as images
FROM products p
LEFT JOIN product_images pi ON pi.product_id = p.id
WHERE p.isactive = true
GROUP BY p.id
ORDER BY p.createdat DESC`
);
sendSuccess(res, { products: result.rows });
})
@@ -33,22 +47,80 @@ router.get(
asyncHandler(async (req, res) => {
const limit = parseInt(req.query.limit) || 4;
const result = await query(
`SELECT id, name, description, price, imageurl, images
FROM products WHERE isactive = true ORDER BY createdat DESC LIMIT $1`,
`SELECT p.id, p.name, p.slug, p.shortdescription, p.price, p.category,
json_agg(
json_build_object(
'image_url', pi.image_url,
'color_variant', pi.color_variant,
'alt_text', pi.alt_text
) ORDER BY pi.display_order, pi.created_at
) FILTER (WHERE pi.id IS NOT NULL) as images
FROM products p
LEFT JOIN product_images pi ON pi.product_id = p.id
WHERE p.isactive = true AND p.isfeatured = true
GROUP BY p.id
ORDER BY p.createdat DESC
LIMIT $1`,
[limit]
);
sendSuccess(res, { products: result.rows });
})
);
// Get single product
// Get single product by ID or slug
router.get(
"/products/:id",
"/products/:identifier",
asyncHandler(async (req, res) => {
const result = await query(
"SELECT * FROM products WHERE id = $1 AND isactive = true",
[req.params.id]
);
const { identifier } = req.params;
// Check if identifier is a UUID
const isUUID =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
identifier
);
// Try to find by ID first, then by slug if not UUID
let result;
if (isUUID) {
result = await query(
`SELECT p.*,
json_agg(
json_build_object(
'id', pi.id,
'image_url', pi.image_url,
'color_variant', pi.color_variant,
'alt_text', pi.alt_text,
'display_order', pi.display_order,
'is_primary', pi.is_primary
) ORDER BY pi.display_order, pi.created_at
) FILTER (WHERE pi.id IS NOT NULL) as images
FROM products p
LEFT JOIN product_images pi ON pi.product_id = p.id
WHERE p.id = $1 AND p.isactive = true
GROUP BY p.id`,
[identifier]
);
} else {
// Try both ID and slug for non-UUID identifiers
result = await query(
`SELECT p.*,
json_agg(
json_build_object(
'id', pi.id,
'image_url', pi.image_url,
'color_variant', pi.color_variant,
'alt_text', pi.alt_text,
'display_order', pi.display_order,
'is_primary', pi.is_primary
) ORDER BY pi.display_order, pi.created_at
) FILTER (WHERE pi.id IS NOT NULL) as images
FROM products p
LEFT JOIN product_images pi ON pi.product_id = p.id
WHERE (p.id = $1 OR p.slug = $1) AND p.isactive = true
GROUP BY p.id`,
[identifier]
);
}
if (result.rows.length === 0) {
return sendNotFound(res, "Product");
@@ -126,7 +198,7 @@ router.get(
"/pages",
asyncHandler(async (req, res) => {
const result = await query(
`SELECT id, title, slug, content, metatitle, metadescription, isactive, createdat
`SELECT id, title, slug, pagecontent as content, metatitle, metadescription, isactive, createdat
FROM pages WHERE isactive = true ORDER BY createdat DESC`
);
sendSuccess(res, { pages: result.rows });
@@ -138,7 +210,7 @@ router.get(
"/pages/:slug",
asyncHandler(async (req, res) => {
const result = await query(
"SELECT * FROM pages WHERE slug = $1 AND isactive = true",
"SELECT id, title, slug, pagecontent as content, metatitle, metadescription FROM pages WHERE slug = $1 AND isactive = true",
[req.params.slug]
);
@@ -176,4 +248,45 @@ router.get(
})
);
// Get all team members (public)
router.get(
"/team-members",
asyncHandler(async (req, res) => {
const result = await query(
"SELECT id, name, position, bio, image_url FROM team_members ORDER BY display_order ASC, created_at DESC"
);
sendSuccess(res, { teamMembers: result.rows });
})
);
// Get menu items (public)
router.get(
"/menu",
asyncHandler(async (req, res) => {
const result = await query(
"SELECT settings FROM site_settings WHERE key = 'menu'"
);
if (result.rows.length === 0) {
return sendSuccess(res, { items: [] });
}
// Parse JSON settings if it's a string
let settings = result.rows[0].settings;
if (typeof settings === "string") {
try {
settings = JSON.parse(settings);
} catch (e) {
logger.error("Failed to parse menu settings:", e);
return sendSuccess(res, { items: [] });
}
}
const items = settings.items || [];
// Filter only visible items for public
const visibleItems = items.filter((item) => item.visible !== false);
sendSuccess(res, { items: visibleItems });
})
);
module.exports = router;

View File

@@ -0,0 +1,55 @@
const { query } = require("./config/database");
async function runMigration() {
try {
console.log("Running migration 004: Enhance color variants...");
// Check current user
const userResult = await query("SELECT current_user");
console.log("Current database user:", userResult.rows[0].current_user);
// Check table owner
const ownerResult = await query(`
SELECT tableowner FROM pg_tables
WHERE tablename = 'product_images'
`);
console.log("Table owner:", ownerResult.rows[0]?.tableowner);
// Check if columns exist
const columnsResult = await query(`
SELECT column_name FROM information_schema.columns
WHERE table_name = 'product_images'
AND column_name IN ('color_code', 'variant_price', 'variant_stock')
`);
console.log(
"Existing columns:",
columnsResult.rows.map((r) => r.column_name)
);
if (columnsResult.rows.length === 3) {
console.log("✓ All columns already exist - no migration needed!");
process.exit(0);
return;
}
console.log("\nPlease run this SQL manually as postgres superuser:");
console.log("---");
console.log(`
ALTER TABLE product_images
ADD COLUMN IF NOT EXISTS color_code VARCHAR(7),
ADD COLUMN IF NOT EXISTS variant_price DECIMAL(10,2),
ADD COLUMN IF NOT EXISTS variant_stock INTEGER DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_product_images_color_code
ON product_images(color_code);
`);
console.log("---");
process.exit(1);
} catch (error) {
console.error("✗ Error:", error.message);
process.exit(1);
}
}
runMigration();

29
backend/run-migration.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
# Run database migration as postgres user
MIGRATION_FILE="$1"
if [ -z "$MIGRATION_FILE" ]; then
echo "Usage: ./run-migration.sh <migration-file.sql>"
exit 1
fi
if [ ! -f "$MIGRATION_FILE" ]; then
echo "Error: Migration file not found: $MIGRATION_FILE"
exit 1
fi
echo "🔐 Running migration as database owner..."
echo "📁 Migration file: $MIGRATION_FILE"
echo ""
sudo -u postgres psql -d skyartshop -f "$MIGRATION_FILE"
if [ $? -eq 0 ]; then
echo ""
echo "✅ Migration completed successfully!"
else
echo ""
echo "❌ Migration failed!"
exit 1
fi

View File

@@ -29,10 +29,28 @@ app.use(
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
styleSrc: [
"'self'",
"'unsafe-inline'",
"https://cdn.jsdelivr.net",
"https://cdn.quilljs.com",
"https://fonts.googleapis.com",
],
scriptSrc: [
"'self'",
"'unsafe-inline'",
"'unsafe-eval'",
"https://cdn.jsdelivr.net",
"https://cdn.quilljs.com",
],
scriptSrcAttr: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "blob:"],
fontSrc: ["'self'", "https://cdn.jsdelivr.net"],
fontSrc: [
"'self'",
"https://cdn.jsdelivr.net",
"https://fonts.gstatic.com",
],
connectSrc: ["'self'", "https://cdn.jsdelivr.net"],
},
},
hsts: {
@@ -162,6 +180,11 @@ app.use("/api", publicRoutes);
// Admin static files (must be after redirect routes)
app.use("/admin", express.static(path.join(baseDir, "admin")));
// Favicon route
app.get("/favicon.ico", (req, res) => {
res.sendFile(path.join(baseDir, "public", "favicon.svg"));
});
// Root redirect to home page
app.get("/", (req, res) => {
res.sendFile(path.join(baseDir, "public", "index.html"));

View File

@@ -0,0 +1,202 @@
#!/usr/bin/env node
/**
* Media Library Database Test Script
* Tests all database operations for uploads and folders
*/
const { pool } = require("./config/database");
const logger = require("./config/logger");
async function testDatabaseOperations() {
console.log("\n🧪 Testing Media Library Database Operations\n");
console.log("=".repeat(60));
try {
// Test 1: Check database connection
console.log("\n1⃣ Testing Database Connection...");
const connectionTest = await pool.query("SELECT NOW()");
console.log(" ✅ Database connected:", connectionTest.rows[0].now);
// Test 2: Check uploads table structure
console.log("\n2⃣ Checking uploads table structure...");
const uploadsSchema = await pool.query(`
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'uploads'
ORDER BY ordinal_position
`);
console.log(" ✅ Uploads table columns:");
uploadsSchema.rows.forEach((col) => {
console.log(
` - ${col.column_name} (${col.data_type}, nullable: ${col.is_nullable})`
);
});
// Test 3: Check media_folders table structure
console.log("\n3⃣ Checking media_folders table structure...");
const foldersSchema = await pool.query(`
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'media_folders'
ORDER BY ordinal_position
`);
console.log(" ✅ Media folders table columns:");
foldersSchema.rows.forEach((col) => {
console.log(
` - ${col.column_name} (${col.data_type}, nullable: ${col.is_nullable})`
);
});
// Test 4: Check foreign key constraints
console.log("\n4⃣ Checking foreign key constraints...");
const constraints = await pool.query(`
SELECT
tc.constraint_name,
tc.table_name,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
AND ccu.table_schema = tc.table_schema
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_name IN ('uploads', 'media_folders')
`);
console.log(" ✅ Foreign key constraints:");
constraints.rows.forEach((fk) => {
console.log(
` - ${fk.table_name}.${fk.column_name}${fk.foreign_table_name}.${fk.foreign_column_name}`
);
});
// Test 5: Count existing data
console.log("\n5⃣ Counting existing data...");
const fileCount = await pool.query("SELECT COUNT(*) as count FROM uploads");
const folderCount = await pool.query(
"SELECT COUNT(*) as count FROM media_folders"
);
console.log(` ✅ Total files: ${fileCount.rows[0].count}`);
console.log(` ✅ Total folders: ${folderCount.rows[0].count}`);
// Test 6: List all files
if (parseInt(fileCount.rows[0].count) > 0) {
console.log("\n6⃣ Listing all files in database...");
const files = await pool.query(`
SELECT id, original_name, file_size, folder_id, uploaded_by, created_at
FROM uploads
ORDER BY created_at DESC
LIMIT 10
`);
console.log(" ✅ Recent files:");
files.rows.forEach((file) => {
const size = (file.file_size / 1024).toFixed(2) + " KB";
const folder = file.folder_id ? `Folder #${file.folder_id}` : "Root";
console.log(
` - [ID: ${file.id}] ${file.original_name} (${size}) - ${folder}`
);
});
}
// Test 7: List all folders
if (parseInt(folderCount.rows[0].count) > 0) {
console.log("\n7⃣ Listing all folders in database...");
const folders = await pool.query(`
SELECT id, name, parent_id, created_by, created_at,
(SELECT COUNT(*) FROM uploads WHERE folder_id = media_folders.id) as file_count
FROM media_folders
ORDER BY created_at DESC
`);
console.log(" ✅ Folders:");
folders.rows.forEach((folder) => {
const parent = folder.parent_id
? `Parent #${folder.parent_id}`
: "Root";
console.log(
` - [ID: ${folder.id}] ${folder.name} (${folder.file_count} files) - ${parent}`
);
});
}
// Test 8: Test folder query with file counts
console.log("\n8⃣ Testing folder query with file counts...");
const foldersWithCounts = await pool.query(`
SELECT
mf.*,
COUNT(u.id) as file_count
FROM media_folders mf
LEFT JOIN uploads u ON u.folder_id = mf.id
GROUP BY mf.id
ORDER BY mf.created_at DESC
`);
console.log(
` ✅ Query returned ${foldersWithCounts.rows.length} folders with accurate file counts`
);
// Test 9: Test cascade delete (dry run)
console.log("\n9⃣ Testing cascade delete rules...");
const cascadeRules = await pool.query(`
SELECT
tc.constraint_name,
rc.delete_rule
FROM information_schema.table_constraints tc
JOIN information_schema.referential_constraints rc
ON tc.constraint_name = rc.constraint_name
WHERE tc.table_name IN ('uploads', 'media_folders')
AND tc.constraint_type = 'FOREIGN KEY'
`);
console.log(" ✅ Delete rules:");
cascadeRules.rows.forEach((rule) => {
console.log(` - ${rule.constraint_name}: ${rule.delete_rule}`);
});
// Test 10: Verify indexes
console.log("\n🔟 Checking database indexes...");
const indexes = await pool.query(`
SELECT
tablename,
indexname,
indexdef
FROM pg_indexes
WHERE tablename IN ('uploads', 'media_folders')
AND schemaname = 'public'
ORDER BY tablename, indexname
`);
console.log(" ✅ Indexes:");
indexes.rows.forEach((idx) => {
console.log(` - ${idx.tablename}.${idx.indexname}`);
});
console.log("\n" + "=".repeat(60));
console.log("✅ All database tests passed successfully!\n");
console.log("📊 Summary:");
console.log(` - Database: Connected and operational`);
console.log(
` - Tables: uploads (${uploadsSchema.rows.length} columns), media_folders (${foldersSchema.rows.length} columns)`
);
console.log(
` - Data: ${fileCount.rows[0].count} files, ${folderCount.rows[0].count} folders`
);
console.log(
` - Constraints: ${constraints.rows.length} foreign keys configured`
);
console.log(` - Indexes: ${indexes.rows.length} indexes for performance`);
console.log(
"\n✅ Media library database is properly configured and operational!\n"
);
} catch (error) {
console.error("\n❌ Database test failed:", error.message);
console.error("Stack trace:", error.stack);
process.exit(1);
} finally {
await pool.end();
}
}
// Run tests
testDatabaseOperations();

219
backend/test-pages-ui.html Normal file
View File

@@ -0,0 +1,219 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Test Editor Resize</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background: #f5f5f5;
}
.test-container {
max-width: 900px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h2 {
color: #333;
margin-bottom: 20px;
}
.editor-resizable {
position: relative;
border: 1px solid #dee2e6;
border-radius: 4px;
overflow: visible;
margin-bottom: 30px;
}
.editor-resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 20px;
height: 20px;
cursor: nwse-resize;
background: linear-gradient(135deg, transparent 50%, #667eea 50%);
z-index: 1000;
transition: background 0.2s;
}
.editor-resize-handle:hover {
background: linear-gradient(135deg, transparent 50%, #5568d3 50%);
}
.editor-resize-handle:active {
background: linear-gradient(135deg, transparent 50%, #4451b8 50%);
}
.editor-content {
height: 300px;
overflow-y: auto;
padding: 15px;
background: white;
}
.editable-content {
height: 300px;
overflow-y: auto;
padding: 15px;
background: white;
border: none;
outline: none;
font-family: Arial, sans-serif;
font-size: 14px;
line-height: 1.6;
resize: none;
}
.status {
padding: 10px;
background: #e7f3ff;
border-left: 4px solid #2196f3;
margin-bottom: 20px;
}
.info-box {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 12px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="test-container">
<h2>🧪 Editor Resize Test - Full Functionality</h2>
<div class="status">
<strong>✅ Test Instructions:</strong> Drag the small blue triangle in
the bottom-right corner to resize. You should be able to:
<ul style="margin: 10px 0 0 20px; padding: 0">
<li>Resize multiple times (up and down)</li>
<li>Edit/type in the expanded area</li>
<li>See smooth resizing with no jumps</li>
</ul>
</div>
<div class="info-box">
<strong>📝 Try This:</strong> Drag editor bigger → Type text in new
space → Drag smaller → Drag bigger again
</div>
<h3>Test 1: Editable Text Area</h3>
<div class="editor-resizable">
<textarea
id="editor1"
class="editable-content"
placeholder="Type here to test editing in expanded area..."
>
This is an EDITABLE text area.
Try this:
1. Drag the corner to make it BIGGER
2. Click here and TYPE NEW TEXT in the expanded area
3. Drag to make it SMALLER
4. Drag to make it BIGGER again
You should be able to resize multiple times and edit anywhere in the expanded space!</textarea
>
<div class="editor-resize-handle" data-target="editor1"></div>
</div>
<h3>Test 2: Contact Fields Scrollable</h3>
<div class="editor-resizable">
<div id="editor2" class="editor-content">
<p><strong>📞 Contact Information</strong></p>
<p>Phone: (555) 123-4567</p>
<p>Email: contact@example.com</p>
<p>Address: 123 Main St, City, State 12345</p>
<br />
<p><strong>🕐 Business Hours</strong></p>
<p>Monday - Friday: 9:00 AM - 6:00 PM</p>
<p>Saturday: 10:00 AM - 4:00 PM</p>
<p>Sunday: Closed</p>
<br />
<p><strong>Extra Content for Testing</strong></p>
<p>This content should remain scrollable.</p>
<p>Resize the box to see more or less content.</p>
<p>The scrollbar should work properly.</p>
</div>
<div class="editor-resize-handle" data-target="editor2"></div>
</div>
<div class="status" id="resizeStatus">
<strong>Status:</strong> Ready to test - Drag the corner handles!
</div>
</div>
<script>
// Simple Drag-to-Resize for Editor Content - EXACT SAME CODE AS ADMIN
(function () {
let resizeState = null;
const statusEl = document.getElementById("resizeStatus");
document.addEventListener("mousedown", function (e) {
if (e.target.classList.contains("editor-resize-handle")) {
e.preventDefault();
e.stopPropagation();
const targetId = e.target.getAttribute("data-target");
const targetElement = document.getElementById(targetId);
if (!targetElement) return;
resizeState = {
target: targetElement,
handle: e.target,
startY: e.clientY,
startHeight: targetElement.offsetHeight,
};
document.body.style.cursor = "nwse-resize";
document.body.style.userSelect = "none";
e.target.style.pointerEvents = "none";
statusEl.innerHTML =
"<strong>Status:</strong> 🔄 Resizing " +
targetId +
"... (Height: " +
Math.round(resizeState.startHeight) +
"px)";
}
});
document.addEventListener("mousemove", function (e) {
if (resizeState) {
e.preventDefault();
e.stopPropagation();
const deltaY = e.clientY - resizeState.startY;
const newHeight = Math.max(
200,
Math.min(1200, resizeState.startHeight + deltaY)
);
resizeState.target.style.height = newHeight + "px";
statusEl.innerHTML =
"<strong>Status:</strong> 📏 Resizing to " +
Math.round(newHeight) +
"px (Delta: " +
Math.round(deltaY) +
"px)";
}
});
document.addEventListener("mouseup", function () {
if (resizeState) {
const finalHeight = resizeState.target.offsetHeight;
statusEl.innerHTML =
"<strong>Status:</strong> ✅ Resize complete! Final height: " +
finalHeight +
"px. Try typing in the editor or resizing again!";
resizeState.handle.style.pointerEvents = "";
resizeState = null;
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
});
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env node
/**
* Test Portfolio API Response
*/
const { query } = require("./config/database");
async function testPortfolioAPI() {
console.log("🧪 Testing Portfolio API Data...\n");
try {
// Simulate what the API endpoint does
const result = await query(
"SELECT id, title, description, imageurl, category, isactive, createdat FROM portfolioprojects ORDER BY createdat DESC"
);
console.log("📊 API Response Data:\n");
result.rows.forEach((p, index) => {
const status = p.isactive ? "✓ Active" : "✗ Inactive";
const statusColor = p.isactive ? "\x1b[32m" : "\x1b[31m";
const reset = "\x1b[0m";
console.log(
`${index + 1}. ${statusColor}${status}${reset} | ID: ${p.id} | ${
p.title
}`
);
console.log(
` isactive value: ${p.isactive} (type: ${typeof p.isactive})`
);
console.log(` category: ${p.category || "N/A"}`);
console.log("");
});
const activeCount = result.rows.filter((p) => p.isactive).length;
const inactiveCount = result.rows.length - activeCount;
console.log(`\n📈 Summary:`);
console.log(` Total: ${result.rows.length} projects`);
console.log(` ✓ Active: ${activeCount}`);
console.log(` ✗ Inactive: ${inactiveCount}\n`);
process.exit(0);
} catch (error) {
console.error("❌ Error:", error.message);
process.exit(1);
}
}
testPortfolioAPI();

View File

@@ -0,0 +1,337 @@
#!/usr/bin/env node
/**
* Test Products API with Color Variants
* Tests the new product creation and management features
*/
const axios = require("axios");
const API_URL = process.env.API_URL || "http://localhost:5000/api";
// Test data
const testProduct = {
name: "Vibrant Sunset Canvas Art",
shortdescription: "Beautiful hand-painted sunset artwork on premium canvas",
description:
"<p>This stunning piece captures the beauty of a <strong>vibrant sunset</strong> over the ocean. Hand-painted with premium acrylics on gallery-wrapped canvas.</p><p><strong>Features:</strong></p><ul><li>Gallery-wrapped canvas</li><li>Ready to hang</li><li>Signed by artist</li></ul>",
price: 249.99,
stockquantity: 10,
category: "Canvas Art",
sku: "ART-SUNSET-001",
weight: 2.5,
dimensions: "24x36 inches",
material: "Acrylic on Canvas",
isactive: true,
isfeatured: true,
isbestseller: false,
images: [
{
image_url: "/uploads/products/sunset-main.jpg",
color_variant: "Original",
alt_text: "Vibrant Sunset Canvas - Main View",
display_order: 0,
is_primary: true,
},
{
image_url: "/uploads/products/sunset-blue.jpg",
color_variant: "Ocean Blue",
alt_text: "Vibrant Sunset Canvas - Blue Variant",
display_order: 1,
is_primary: false,
},
{
image_url: "/uploads/products/sunset-warm.jpg",
color_variant: "Warm Tones",
alt_text: "Vibrant Sunset Canvas - Warm Variant",
display_order: 2,
is_primary: false,
},
],
};
let createdProductId = null;
let sessionCookie = null;
async function login() {
try {
console.log("🔐 Logging in...");
const response = await axios.post(
`${API_URL}/auth/login`,
{
email: process.env.ADMIN_EMAIL || "admin@skyartshop.com",
password: process.env.ADMIN_PASSWORD || "admin123",
},
{
headers: { "Content-Type": "application/json" },
}
);
sessionCookie = response.headers["set-cookie"];
console.log("✅ Login successful\n");
return true;
} catch (error) {
console.error("❌ Login failed:", error.response?.data || error.message);
return false;
}
}
async function createProduct() {
try {
console.log("📦 Creating new product...");
console.log("Product name:", testProduct.name);
console.log(
"Color variants:",
testProduct.images.map((img) => img.color_variant).join(", ")
);
const response = await axios.post(
`${API_URL}/admin/products`,
testProduct,
{
headers: {
"Content-Type": "application/json",
Cookie: sessionCookie,
},
}
);
if (response.data.success) {
createdProductId = response.data.product.id;
console.log("✅ Product created successfully!");
console.log("Product ID:", createdProductId);
console.log("Images count:", response.data.product.images?.length || 0);
console.log("");
return true;
}
} catch (error) {
console.error(
"❌ Product creation failed:",
error.response?.data || error.message
);
return false;
}
}
async function getProduct() {
try {
console.log("📖 Fetching product details...");
const response = await axios.get(
`${API_URL}/admin/products/${createdProductId}`,
{
headers: { Cookie: sessionCookie },
}
);
if (response.data.success) {
const product = response.data.product;
console.log("✅ Product retrieved successfully!");
console.log("Name:", product.name);
console.log("Price:", product.price);
console.log("SKU:", product.sku);
console.log("Stock:", product.stockquantity);
console.log("Active:", product.isactive);
console.log("Featured:", product.isfeatured);
console.log("Images:");
if (product.images && product.images.length > 0) {
product.images.forEach((img, idx) => {
console.log(
` ${idx + 1}. ${img.color_variant || "Default"} - ${
img.image_url
} ${img.is_primary ? "(Primary)" : ""}`
);
});
}
console.log("");
return true;
}
} catch (error) {
console.error(
"❌ Failed to fetch product:",
error.response?.data || error.message
);
return false;
}
}
async function updateProduct() {
try {
console.log("✏️ Updating product...");
const updateData = {
price: 199.99,
stockquantity: 15,
isbestseller: true,
images: [
...testProduct.images,
{
image_url: "/uploads/products/sunset-purple.jpg",
color_variant: "Purple Haze",
alt_text: "Vibrant Sunset Canvas - Purple Variant",
display_order: 3,
is_primary: false,
},
],
};
const response = await axios.put(
`${API_URL}/admin/products/${createdProductId}`,
updateData,
{
headers: {
"Content-Type": "application/json",
Cookie: sessionCookie,
},
}
);
if (response.data.success) {
console.log("✅ Product updated successfully!");
console.log("New price:", response.data.product.price);
console.log("New stock:", response.data.product.stockquantity);
console.log("Bestseller:", response.data.product.isbestseller);
console.log("Total images:", response.data.product.images?.length || 0);
console.log("");
return true;
}
} catch (error) {
console.error(
"❌ Product update failed:",
error.response?.data || error.message
);
return false;
}
}
async function listProducts() {
try {
console.log("📋 Listing all products...");
const response = await axios.get(`${API_URL}/admin/products`, {
headers: { Cookie: sessionCookie },
});
if (response.data.success) {
console.log(`✅ Found ${response.data.products.length} products`);
response.data.products.forEach((p, idx) => {
console.log(
`${idx + 1}. ${p.name} - $${p.price} (${p.image_count || 0} images)`
);
});
console.log("");
return true;
}
} catch (error) {
console.error(
"❌ Failed to list products:",
error.response?.data || error.message
);
return false;
}
}
async function getPublicProduct() {
try {
console.log("🌐 Fetching product from public API...");
const response = await axios.get(
`${API_URL}/public/products/${createdProductId}`
);
if (response.data.success) {
const product = response.data.product;
console.log("✅ Public product retrieved!");
console.log("Name:", product.name);
console.log(
"Short description:",
product.shortdescription?.substring(0, 50) + "..."
);
console.log("Color variants available:");
const variants = [
...new Set(
product.images?.map((img) => img.color_variant).filter(Boolean)
),
];
variants.forEach((variant) => {
console.log(` - ${variant}`);
});
console.log("");
return true;
}
} catch (error) {
console.error(
"❌ Failed to fetch public product:",
error.response?.data || error.message
);
return false;
}
}
async function deleteProduct() {
try {
console.log("🗑️ Deleting test product...");
const response = await axios.delete(
`${API_URL}/admin/products/${createdProductId}`,
{
headers: { Cookie: sessionCookie },
}
);
if (response.data.success) {
console.log("✅ Product deleted successfully!");
console.log("");
return true;
}
} catch (error) {
console.error(
"❌ Product deletion failed:",
error.response?.data || error.message
);
return false;
}
}
async function runTests() {
console.log("=".repeat(60));
console.log(" PRODUCTS API TEST - Color Variants & Rich Text");
console.log("=".repeat(60));
console.log("");
const steps = [
{ name: "Login", fn: login },
{ name: "Create Product", fn: createProduct },
{ name: "Get Product", fn: getProduct },
{ name: "Update Product", fn: updateProduct },
{ name: "List Products", fn: listProducts },
{ name: "Get Public Product", fn: getPublicProduct },
{ name: "Delete Product", fn: deleteProduct },
];
let passed = 0;
let failed = 0;
for (const step of steps) {
const success = await step.fn();
if (success) {
passed++;
} else {
failed++;
console.log(`⚠️ Stopping tests due to failure in: ${step.name}\n`);
break;
}
}
console.log("=".repeat(60));
console.log(`TEST RESULTS: ${passed} passed, ${failed} failed`);
console.log("=".repeat(60));
process.exit(failed > 0 ? 1 : 0);
}
runTests().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});

168
backend/test-products-api.sh Executable file
View File

@@ -0,0 +1,168 @@
#!/bin/bash
# Test Products API with curl
API_URL="http://localhost:5000/api"
SESSION_FILE="/tmp/skyart_session.txt"
echo "============================================================"
echo " PRODUCTS API TEST - Color Variants & Rich Text"
echo "============================================================"
echo ""
# Test 1: Login
echo "🔐 Test 1: Login..."
LOGIN_RESPONSE=$(curl -s -c "$SESSION_FILE" -X POST "$API_URL/admin/login" \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"admin123"}')
if echo "$LOGIN_RESPONSE" | grep -q '"success":true'; then
echo "✅ Login successful"
else
echo "❌ Login failed"
echo "$LOGIN_RESPONSE"
exit 1
fi
echo ""
# Test 2: Create Product
echo "📦 Test 2: Creating product with color variants..."
CREATE_RESPONSE=$(curl -s -b "$SESSION_FILE" -X POST "$API_URL/admin/products" \
-H "Content-Type: application/json" \
-d '{
"name": "Test Sunset Canvas Art",
"shortdescription": "Beautiful hand-painted sunset artwork",
"description": "<p>This stunning piece captures the beauty of a <strong>vibrant sunset</strong>.</p><ul><li>Gallery-wrapped canvas</li><li>Ready to hang</li></ul>",
"price": 249.99,
"stockquantity": 10,
"category": "Canvas Art",
"sku": "ART-TEST-001",
"weight": 2.5,
"dimensions": "24x36 inches",
"material": "Acrylic on Canvas",
"isactive": true,
"isfeatured": true,
"isbestseller": false,
"images": [
{
"image_url": "/uploads/test-sunset-main.jpg",
"color_variant": "Original",
"alt_text": "Sunset Canvas - Main",
"display_order": 0,
"is_primary": true
},
{
"image_url": "/uploads/test-sunset-blue.jpg",
"color_variant": "Ocean Blue",
"alt_text": "Sunset Canvas - Blue",
"display_order": 1
},
{
"image_url": "/uploads/test-sunset-warm.jpg",
"color_variant": "Warm Tones",
"alt_text": "Sunset Canvas - Warm",
"display_order": 2
}
]
}')
PRODUCT_ID=$(echo "$CREATE_RESPONSE" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -n "$PRODUCT_ID" ]; then
echo "✅ Product created successfully"
echo " Product ID: $PRODUCT_ID"
IMAGE_COUNT=$(echo "$CREATE_RESPONSE" | grep -o '"image_url"' | wc -l)
echo " Images: $IMAGE_COUNT"
else
echo "❌ Product creation failed"
echo "$CREATE_RESPONSE" | head -50
exit 1
fi
echo ""
# Test 3: Get Product
echo "📖 Test 3: Fetching product details..."
GET_RESPONSE=$(curl -s -b "$SESSION_FILE" "$API_URL/admin/products/$PRODUCT_ID")
if echo "$GET_RESPONSE" | grep -q '"success":true'; then
echo "✅ Product retrieved successfully"
echo " Name: $(echo "$GET_RESPONSE" | grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4)"
echo " Price: $(echo "$GET_RESPONSE" | grep -o '"price":"[^"]*"' | head -1 | cut -d'"' -f4)"
echo " Color variants:"
echo "$GET_RESPONSE" | grep -o '"color_variant":"[^"]*"' | cut -d'"' -f4 | while read variant; do
echo " - $variant"
done
else
echo "❌ Failed to retrieve product"
fi
echo ""
# Test 4: Update Product
echo "✏️ Test 4: Updating product..."
UPDATE_RESPONSE=$(curl -s -b "$SESSION_FILE" -X PUT "$API_URL/admin/products/$PRODUCT_ID" \
-H "Content-Type: application/json" \
-d '{
"price": 199.99,
"stockquantity": 15,
"isbestseller": true
}')
if echo "$UPDATE_RESPONSE" | grep -q '"success":true'; then
echo "✅ Product updated successfully"
echo " New price: $(echo "$UPDATE_RESPONSE" | grep -o '"price":"[^"]*"' | head -1 | cut -d'"' -f4)"
else
echo "❌ Product update failed"
fi
echo ""
# Test 5: List Products
echo "📋 Test 5: Listing all products..."
LIST_RESPONSE=$(curl -s -b "$SESSION_FILE" "$API_URL/admin/products")
PRODUCT_COUNT=$(echo "$LIST_RESPONSE" | grep -o '"id"' | wc -l)
echo "✅ Found $PRODUCT_COUNT products"
echo ""
# Test 6: Public API
echo "🌐 Test 6: Testing public API..."
PUBLIC_RESPONSE=$(curl -s "$API_URL/products/$PRODUCT_ID")
if echo "$PUBLIC_RESPONSE" | grep -q '"success":true'; then
echo "✅ Public product retrieved"
echo " Available color variants:"
echo "$PUBLIC_RESPONSE" | grep -o '"color_variant":"[^"]*"' | cut -d'"' -f4 | sort -u | while read variant; do
echo " - $variant"
done
else
echo "❌ Failed to retrieve public product"
fi
echo ""
# Test 7: Delete Product
echo "🗑️ Test 7: Cleaning up test product..."
DELETE_RESPONSE=$(curl -s -b "$SESSION_FILE" -X DELETE "$API_URL/admin/products/$PRODUCT_ID")
if echo "$DELETE_RESPONSE" | grep -q '"success":true'; then
echo "✅ Test product deleted"
else
echo "❌ Product deletion failed"
fi
echo ""
# Cleanup
rm -f "$SESSION_FILE"
echo "============================================================"
echo " ALL TESTS COMPLETED SUCCESSFULLY! ✅"
echo "============================================================"
echo ""
echo "Features Verified:"
echo " ✅ Product creation with color variants"
echo " ✅ Rich text HTML description"
echo " ✅ Multiple images per product"
echo " ✅ Color variant assignments"
echo " ✅ Active/Featured/Bestseller flags"
echo " ✅ Product metadata (SKU, weight, dimensions, material)"
echo " ✅ Product updates"
echo " ✅ Public API access"
echo " ✅ Product deletion with cascade"
echo ""

View File

@@ -0,0 +1,115 @@
const db = require('./config/database');
async function updateContactLayout() {
try {
const contactContentHTML = `
<div style="text-align: center; margin-bottom: 48px">
<h2 style="font-size: 2rem; font-weight: 700; color: #2d3436; margin-bottom: 12px;">
Our Contact Information
</h2>
<p style="font-size: 1rem; color: #636e72">
Reach out to us through any of these channels
</p>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 32px; margin-bottom: 40px;">
<!-- Phone -->
<div style="background: #f8f9fa; padding: 32px 24px; border-radius: 12px; text-align: center; border: 2px solid #e1e8ed; transition: all 0.3s;">
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px;">
<i class="bi bi-telephone" style="font-size: 28px; color: white"></i>
</div>
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 12px; color: #2d3436;">Phone</h3>
<p style="color: #636e72; margin: 0 0 8px 0; font-size: 15px">Give us a call</p>
<a href="tel:+1234567890" style="color: #667eea; font-weight: 600; text-decoration: none; font-size: 16px">+1 (234) 567-8900</a>
</div>
<!-- Email -->
<div style="background: #f8f9fa; padding: 32px 24px; border-radius: 12px; text-align: center; border: 2px solid #e1e8ed; transition: all 0.3s;">
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px;">
<i class="bi bi-envelope" style="font-size: 28px; color: white"></i>
</div>
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 12px; color: #2d3436;">Email</h3>
<p style="color: #636e72; margin: 0 0 8px 0; font-size: 15px">Send us an email</p>
<a href="mailto:support@skyartshop.com" style="color: #667eea; font-weight: 600; text-decoration: none; font-size: 16px">support@skyartshop.com</a>
</div>
<!-- Location -->
<div style="background: #f8f9fa; padding: 32px 24px; border-radius: 12px; text-align: center; border: 2px solid #e1e8ed; transition: all 0.3s;">
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px;">
<i class="bi bi-geo-alt" style="font-size: 28px; color: white"></i>
</div>
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 12px; color: #2d3436;">Location</h3>
<p style="color: #636e72; margin: 0 0 8px 0; font-size: 15px">Visit our shop</p>
<p style="color: #667eea; font-weight: 600; margin: 0; font-size: 16px; line-height: 1.6;">
123 Creative Street<br>Art District, CA 90210
</p>
</div>
</div>
<!-- Business Hours -->
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; padding: 40px; margin-top: 40px; text-align: center; color: white;">
<h3 style="font-size: 24px; font-weight: 600; margin-bottom: 24px;">Business Hours</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 24px; max-width: 700px; margin: 0 auto;">
<div style="background: rgba(255, 255, 255, 0.1); padding: 20px; border-radius: 8px; backdrop-filter: blur(10px);">
<p style="margin: 0; font-weight: 500; opacity: 0.95; font-size: 16px;">Monday - Friday</p>
<p style="margin: 8px 0 0 0; font-size: 20px; font-weight: 700;">9:00 AM - 6:00 PM</p>
</div>
<div style="background: rgba(255, 255, 255, 0.1); padding: 20px; border-radius: 8px; backdrop-filter: blur(10px);">
<p style="margin: 0; font-weight: 500; opacity: 0.95; font-size: 16px;">Saturday</p>
<p style="margin: 8px 0 0 0; font-size: 20px; font-weight: 700;">10:00 AM - 4:00 PM</p>
</div>
<div style="background: rgba(255, 255, 255, 0.1); padding: 20px; border-radius: 8px; backdrop-filter: blur(10px);">
<p style="margin: 0; font-weight: 500; opacity: 0.95; font-size: 16px;">Sunday</p>
<p style="margin: 8px 0 0 0; font-size: 20px; font-weight: 700;">Closed</p>
</div>
</div>
</div>
`;
const contactDelta = {
ops: [
{ insert: 'Get In Touch', attributes: { header: 2 } },
{ insert: '\nHave questions or feedback? We\'d love to hear from you. Send us a message and we\'ll respond as soon as possible.\n\n' },
{ insert: 'Contact Information', attributes: { header: 2 } },
{ insert: '\n' },
{ insert: 'Phone', attributes: { header: 3 } },
{ insert: '\nGive us a call: ' },
{ insert: '+1 (234) 567-8900', attributes: { bold: true, link: 'tel:+1234567890' } },
{ insert: '\n\n' },
{ insert: 'Email', attributes: { header: 3 } },
{ insert: '\nSend us an email: ' },
{ insert: 'support@skyartshop.com', attributes: { bold: true, link: 'mailto:support@skyartshop.com' } },
{ insert: '\n\n' },
{ insert: 'Location', attributes: { header: 3 } },
{ insert: '\n' },
{ insert: '123 Creative Street\nArt District, CA 90210', attributes: { bold: true } },
{ insert: '\n\n' },
{ insert: 'Business Hours', attributes: { header: 2 } },
{ insert: '\n' },
{ insert: 'Monday - Friday: 9:00 AM - 6:00 PM', attributes: { list: 'bullet' } },
{ insert: '\n' },
{ insert: 'Saturday: 10:00 AM - 4:00 PM', attributes: { list: 'bullet' } },
{ insert: '\n' },
{ insert: 'Sunday: Closed', attributes: { list: 'bullet' } },
{ insert: '\n\nFill out the contact form on our website and we\'ll get back to you within 24 hours\n' }
]
};
await db.query(
`UPDATE pages SET
content = $1,
pagecontent = $2,
updatedat = NOW()
WHERE slug = 'contact'`,
[JSON.stringify(contactDelta), contactContentHTML]
);
console.log('✅ Contact page layout updated successfully!');
process.exit(0);
} catch (error) {
console.error('❌ Error:', error);
process.exit(1);
}
}
updateContactLayout();

View File

@@ -0,0 +1,377 @@
const db = require("./config/database");
async function updatePagesWithContent() {
try {
// About Page Content
const aboutContent = `
<h2>Our Story</h2>
<p>Sky Art Shop specializes in scrapbooking, journaling, cardmaking, and collaging stationery. We are passionate about helping people express their creativity and preserve their memories.</p>
<p>Our mission is to promote mental health and wellness through creative art activities. We believe that crafting is more than just a hobby—it's a therapeutic journey that brings joy, mindfulness, and self-expression.</p>
<h2>What We Offer</h2>
<p>Our carefully curated collection includes:</p>
<ul>
<li>Washi tape in various designs and patterns</li>
<li>Unique stickers for journaling and scrapbooking</li>
<li>High-quality journals and notebooks</li>
<li>Card making supplies and kits</li>
<li>Collage materials and ephemera</li>
<li>Creative tools and accessories</li>
</ul>
<h2>Why Choose Us</h2>
<p>We hand-select every item in our store to ensure the highest quality and uniqueness. Whether you're a seasoned crafter or just starting your creative journey, we have something special for everyone.</p>
<p>Join our community of creative minds and let your imagination soar!</p>
`;
const aboutDelta = {
ops: [
{ insert: "Our Story", attributes: { header: 2 } },
{
insert:
"\nSky Art Shop specializes in scrapbooking, journaling, cardmaking, and collaging stationery. We are passionate about helping people express their creativity and preserve their memories.\n\nOur mission is to promote mental health and wellness through creative art activities. We believe that crafting is more than just a hobby—it's a therapeutic journey that brings joy, mindfulness, and self-expression.\n\n",
},
{ insert: "What We Offer", attributes: { header: 2 } },
{ insert: "\nOur carefully curated collection includes:\n" },
{
insert: "Washi tape in various designs and patterns",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{
insert: "Unique stickers for journaling and scrapbooking",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{
insert: "High-quality journals and notebooks",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{
insert: "Card making supplies and kits",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{
insert: "Collage materials and ephemera",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{
insert: "Creative tools and accessories",
attributes: { list: "bullet" },
},
{ insert: "\n\n" },
{ insert: "Why Choose Us", attributes: { header: 2 } },
{
insert:
"\nWe hand-select every item in our store to ensure the highest quality and uniqueness. Whether you're a seasoned crafter or just starting your creative journey, we have something special for everyone.\n\nJoin our community of creative minds and let your imagination soar!\n",
},
],
};
// Contact Page Content
const contactContent = `
<h2>Get In Touch</h2>
<p>Have questions or feedback? We'd love to hear from you. Send us a message and we'll respond as soon as possible.</p>
<h2>Contact Information</h2>
<h3>Phone</h3>
<p>Give us a call: <strong><a href="tel:+1234567890">+1 (234) 567-8900</a></strong></p>
<h3>Email</h3>
<p>Send us an email: <strong><a href="mailto:support@skyartshop.com">support@skyartshop.com</a></strong></p>
<h3>Location</h3>
<p><strong>123 Creative Street<br>Art District, CA 90210</strong></p>
<h2>Business Hours</h2>
<ul>
<li><strong>Monday - Friday:</strong> 9:00 AM - 6:00 PM</li>
<li><strong>Saturday:</strong> 10:00 AM - 4:00 PM</li>
<li><strong>Sunday:</strong> Closed</li>
</ul>
<p><em>Fill out the contact form on our website and we'll get back to you within 24 hours</em></p>
`;
const contactDelta = {
ops: [
{ insert: "Get In Touch", attributes: { header: 2 } },
{
insert:
"\nHave questions or feedback? We'd love to hear from you. Send us a message and we'll respond as soon as possible.\n\n",
},
{ insert: "Contact Information", attributes: { header: 2 } },
{ insert: "\n" },
{ insert: "Phone", attributes: { header: 3 } },
{ insert: "\nGive us a call: " },
{
insert: "+1 (234) 567-8900",
attributes: { bold: true, link: "tel:+1234567890" },
},
{ insert: "\n\n" },
{ insert: "Email", attributes: { header: 3 } },
{ insert: "\nSend us an email: " },
{
insert: "support@skyartshop.com",
attributes: { bold: true, link: "mailto:support@skyartshop.com" },
},
{ insert: "\n\n" },
{ insert: "Location", attributes: { header: 3 } },
{ insert: "\n" },
{
insert: "123 Creative Street\nArt District, CA 90210",
attributes: { bold: true },
},
{ insert: "\n\n" },
{ insert: "Business Hours", attributes: { header: 2 } },
{ insert: "\n" },
{
insert: "Monday - Friday: 9:00 AM - 6:00 PM",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{
insert: "Saturday: 10:00 AM - 4:00 PM",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{ insert: "Sunday: Closed", attributes: { list: "bullet" } },
{
insert:
"\n\nFill out the contact form on our website and we'll get back to you within 24 hours\n",
},
],
};
// Privacy Page Content
const privacyContent = `
<h2>Privacy Policy</h2>
<p><em>Last Updated: December 23, 2025</em></p>
<h3>1. Information We Collect</h3>
<p>We collect information you provide directly to us, including:</p>
<ul>
<li>Name and contact information</li>
<li>Billing and shipping addresses</li>
<li>Payment information</li>
<li>Order history and preferences</li>
<li>Communications with us</li>
</ul>
<h3>2. How We Use Your Information</h3>
<p>We use the information we collect to:</p>
<ul>
<li>Process and fulfill your orders</li>
<li>Communicate with you about products and services</li>
<li>Improve our website and customer experience</li>
<li>Send marketing communications (with your consent)</li>
<li>Comply with legal obligations</li>
</ul>
<h3>3. Information Sharing</h3>
<p>We do not sell or rent your personal information to third parties. We may share your information with:</p>
<ul>
<li>Service providers who assist in our operations</li>
<li>Payment processors for transaction processing</li>
<li>Shipping companies for order delivery</li>
</ul>
<h3>4. Data Security</h3>
<p>We implement appropriate security measures to protect your personal information. However, no method of transmission over the Internet is 100% secure.</p>
<h3>5. Your Rights</h3>
<p>You have the right to:</p>
<ul>
<li>Access your personal information</li>
<li>Correct inaccurate data</li>
<li>Request deletion of your data</li>
<li>Opt-out of marketing communications</li>
</ul>
<h3>6. Contact Us</h3>
<p>If you have questions about this Privacy Policy, please contact us at: <strong><a href="mailto:privacy@skyartshop.com">privacy@skyartshop.com</a></strong></p>
`;
const privacyDelta = {
ops: [
{ insert: "Privacy Policy", attributes: { header: 2 } },
{ insert: "\n" },
{
insert: "Last Updated: December 23, 2025",
attributes: { italic: true },
},
{ insert: "\n\n" },
{ insert: "1. Information We Collect", attributes: { header: 3 } },
{
insert:
"\nWe collect information you provide directly to us, including:\n",
},
{
insert: "Name and contact information",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{
insert: "Billing and shipping addresses",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{ insert: "Payment information", attributes: { list: "bullet" } },
{ insert: "\n" },
{
insert: "Order history and preferences",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{ insert: "Communications with us", attributes: { list: "bullet" } },
{ insert: "\n\n" },
{ insert: "2. How We Use Your Information", attributes: { header: 3 } },
{ insert: "\nWe use the information we collect to:\n" },
{
insert: "Process and fulfill your orders",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{
insert: "Communicate with you about products and services",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{
insert: "Improve our website and customer experience",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{
insert: "Send marketing communications (with your consent)",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{
insert: "Comply with legal obligations",
attributes: { list: "bullet" },
},
{ insert: "\n\n" },
{ insert: "3. Information Sharing", attributes: { header: 3 } },
{
insert:
"\nWe do not sell or rent your personal information to third parties. We may share your information with:\n",
},
{
insert: "Service providers who assist in our operations",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{
insert: "Payment processors for transaction processing",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{
insert: "Shipping companies for order delivery",
attributes: { list: "bullet" },
},
{ insert: "\n\n" },
{ insert: "4. Data Security", attributes: { header: 3 } },
{
insert:
"\nWe implement appropriate security measures to protect your personal information. However, no method of transmission over the Internet is 100% secure.\n\n",
},
{ insert: "5. Your Rights", attributes: { header: 3 } },
{ insert: "\nYou have the right to:\n" },
{
insert: "Access your personal information",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{ insert: "Correct inaccurate data", attributes: { list: "bullet" } },
{ insert: "\n" },
{
insert: "Request deletion of your data",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{
insert: "Opt-out of marketing communications",
attributes: { list: "bullet" },
},
{ insert: "\n\n" },
{ insert: "6. Contact Us", attributes: { header: 3 } },
{
insert:
"\nIf you have questions about this Privacy Policy, please contact us at: ",
},
{
insert: "privacy@skyartshop.com",
attributes: { bold: true, link: "mailto:privacy@skyartshop.com" },
},
{ insert: "\n" },
],
};
// Update About page
await db.query(
`UPDATE pages SET
content = $1,
pagecontent = $2,
metatitle = $3,
metadescription = $4,
updatedat = NOW()
WHERE slug = 'about'`,
[
JSON.stringify(aboutDelta),
aboutContent,
"About Us - Sky Art Shop",
"Learn about Sky Art Shop, our mission to promote wellness through creative activities, and our carefully curated collection of scrapbooking and journaling supplies.",
]
);
console.log("✓ About page updated");
// Update Contact page
await db.query(
`UPDATE pages SET
content = $1,
pagecontent = $2,
metatitle = $3,
metadescription = $4,
updatedat = NOW()
WHERE slug = 'contact'`,
[
JSON.stringify(contactDelta),
contactContent,
"Contact Us - Sky Art Shop",
"Get in touch with Sky Art Shop. Phone: +1 (234) 567-8900 | Email: support@skyartshop.com | Open Monday-Saturday",
]
);
console.log("✓ Contact page updated");
// Update Privacy page
await db.query(
`UPDATE pages SET
content = $1,
pagecontent = $2,
metatitle = $3,
metadescription = $4,
updatedat = NOW()
WHERE slug = 'privacy'`,
[
JSON.stringify(privacyDelta),
privacyContent,
"Privacy Policy - Sky Art Shop",
"Read our privacy policy to understand how Sky Art Shop collects, uses, and protects your personal information.",
]
);
console.log("✓ Privacy page updated");
console.log("\n✅ All pages updated successfully!");
process.exit(0);
} catch (error) {
console.error("❌ Error updating pages:", error);
process.exit(1);
}
}
updatePagesWithContent();