331 lines
10 KiB
JavaScript
331 lines
10 KiB
JavaScript
const express = require("express");
|
|
const { query } = require("../config/database");
|
|
const logger = require("../config/logger");
|
|
const { asyncHandler } = require("../middleware/errorHandler");
|
|
const { cacheMiddleware, cache } = require("../middleware/cache");
|
|
const {
|
|
sendSuccess,
|
|
sendError,
|
|
sendNotFound,
|
|
} = require("../utils/responseHelpers");
|
|
const router = express.Router();
|
|
|
|
const handleDatabaseError = (res, error, context) => {
|
|
logger.error(`${context} error:`, error);
|
|
sendError(res);
|
|
};
|
|
|
|
// Get all products - Cached for 5 minutes
|
|
router.get(
|
|
"/products",
|
|
cacheMiddleware(300000), // 5 minutes cache
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
`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,
|
|
COALESCE(
|
|
json_agg(
|
|
json_build_object(
|
|
'id', pi.id,
|
|
'image_url', pi.image_url,
|
|
'color_variant', pi.color_variant,
|
|
'color_code', pi.color_code,
|
|
'alt_text', pi.alt_text,
|
|
'is_primary', pi.is_primary,
|
|
'variant_price', pi.variant_price,
|
|
'variant_stock', pi.variant_stock
|
|
) ORDER BY pi.display_order, pi.created_at
|
|
) FILTER (WHERE pi.id IS NOT NULL),
|
|
'[]'::json
|
|
) 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 });
|
|
})
|
|
);
|
|
|
|
// Get featured products - Cached for 10 minutes
|
|
router.get(
|
|
"/products/featured",
|
|
cacheMiddleware(600000, (req) => `featured:${req.query.limit || 4}`), // 10 minutes cache
|
|
asyncHandler(async (req, res) => {
|
|
const limit = Math.min(parseInt(req.query.limit) || 4, 20); // Max 20 items
|
|
const result = await query(
|
|
`SELECT p.id, p.name, p.slug, p.shortdescription, p.price, p.category, p.stockquantity,
|
|
COALESCE(
|
|
json_agg(
|
|
json_build_object(
|
|
'image_url', pi.image_url,
|
|
'color_variant', pi.color_variant,
|
|
'color_code', pi.color_code,
|
|
'alt_text', pi.alt_text,
|
|
'variant_price', pi.variant_price,
|
|
'variant_stock', pi.variant_stock
|
|
) ORDER BY pi.display_order, pi.created_at
|
|
) FILTER (WHERE pi.id IS NOT NULL),
|
|
'[]'::json
|
|
) 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 by ID or slug
|
|
router.get(
|
|
"/products/:identifier",
|
|
asyncHandler(async (req, res) => {
|
|
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,
|
|
'color_code', pi.color_code,
|
|
'alt_text', pi.alt_text,
|
|
'display_order', pi.display_order,
|
|
'is_primary', pi.is_primary,
|
|
'variant_price', pi.variant_price,
|
|
'variant_stock', pi.variant_stock
|
|
) 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,
|
|
'color_code', pi.color_code,
|
|
'alt_text', pi.alt_text,
|
|
'display_order', pi.display_order,
|
|
'is_primary', pi.is_primary,
|
|
'variant_price', pi.variant_price,
|
|
'variant_stock', pi.variant_stock
|
|
) 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");
|
|
}
|
|
|
|
sendSuccess(res, { product: result.rows[0] });
|
|
})
|
|
);
|
|
|
|
// Get all product categories - Cached for 30 minutes
|
|
router.get(
|
|
"/categories",
|
|
cacheMiddleware(1800000), // 30 minutes cache
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
`SELECT DISTINCT category
|
|
FROM products
|
|
WHERE isactive = true AND category IS NOT NULL AND category != ''
|
|
ORDER BY category ASC`
|
|
);
|
|
sendSuccess(res, { categories: result.rows.map((row) => row.category) });
|
|
})
|
|
);
|
|
|
|
// Get site settings
|
|
router.get(
|
|
"/settings",
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query("SELECT * FROM sitesettings LIMIT 1");
|
|
sendSuccess(res, { settings: result.rows[0] || {} });
|
|
})
|
|
);
|
|
|
|
// Get homepage sections - Cached for 15 minutes
|
|
router.get(
|
|
"/homepage/sections",
|
|
cacheMiddleware(900000), // 15 minutes cache
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
"SELECT * FROM homepagesections ORDER BY displayorder ASC"
|
|
);
|
|
sendSuccess(res, { sections: result.rows });
|
|
})
|
|
);
|
|
// Get portfolio projects - Cached for 10 minutes
|
|
router.get(
|
|
"/portfolio/projects",
|
|
cacheMiddleware(600000), // 10 minutes cache
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
`SELECT id, title, description, featuredimage, images, category,
|
|
categoryid, isactive, createdat
|
|
FROM portfolioprojects WHERE isactive = true
|
|
ORDER BY displayorder ASC, createdat DESC`
|
|
);
|
|
sendSuccess(res, { projects: result.rows });
|
|
})
|
|
);
|
|
|
|
// Get blog posts - Cached for 5 minutes
|
|
router.get(
|
|
"/blog/posts",
|
|
cacheMiddleware(300000), // 5 minutes cache
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
`SELECT id, title, slug, excerpt, content, imageurl, ispublished, createdat
|
|
FROM blogposts WHERE ispublished = true ORDER BY createdat DESC`
|
|
);
|
|
sendSuccess(res, { posts: result.rows });
|
|
})
|
|
);
|
|
|
|
// Get single blog post by slug
|
|
router.get(
|
|
"/blog/posts/:slug",
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
"SELECT * FROM blogposts WHERE slug = $1 AND ispublished = true",
|
|
[req.params.slug]
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
return sendNotFound(res, "Blog post");
|
|
}
|
|
|
|
sendSuccess(res, { post: result.rows[0] });
|
|
})
|
|
);
|
|
|
|
// Get custom pages
|
|
router.get(
|
|
"/pages",
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
`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 });
|
|
})
|
|
);
|
|
|
|
// Get single page by slug
|
|
router.get(
|
|
"/pages/:slug",
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
"SELECT id, title, slug, pagecontent as content, metatitle, metadescription FROM pages WHERE slug = $1 AND isactive = true",
|
|
[req.params.slug]
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
return sendNotFound(res, "Page");
|
|
}
|
|
|
|
sendSuccess(res, { page: result.rows[0] });
|
|
})
|
|
);
|
|
|
|
// Get menu items for frontend navigation
|
|
router.get(
|
|
"/menu",
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
"SELECT settings FROM site_settings WHERE key = 'menu'"
|
|
);
|
|
const items =
|
|
result.rows.length > 0 ? result.rows[0].settings.items || [] : [];
|
|
const visibleItems = items.filter((item) => item.visible !== false);
|
|
sendSuccess(res, { items: visibleItems });
|
|
})
|
|
);
|
|
|
|
// Get homepage settings for frontend
|
|
router.get(
|
|
"/homepage/settings",
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
"SELECT settings FROM site_settings WHERE key = 'homepage'"
|
|
);
|
|
const settings = result.rows.length > 0 ? result.rows[0].settings : {};
|
|
sendSuccess(res, { settings });
|
|
})
|
|
);
|
|
|
|
// 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;
|