const mongoose = require('mongoose'); /** * نموذج الأكواد المختصرة - يدعم الـ Shortcodes المخصصة والمدمجة * @version 2.0.0 */ const shortcodeParameterSchema = new mongoose.Schema({ name: { type: String, required: true }, type: { type: String, enum: ['string', 'number', 'boolean', 'array'], default: 'string' }, required: { type: Boolean, default: false }, defaultValue: { type: mongoose.Schema.Types.Mixed }, description: { type: String } }, { _id: false }); const storeShortcodeSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true }, // معلومات الـ Shortcode name: { type: String, required: true, trim: true, lowercase: true, match: /^[a-z][a-z0-9_-]*$/, index: true }, displayName: { type: String, required: true, trim: true, maxlength: 50 }, description: { type: String, maxlength: 500 }, // نوع الـ Shortcode type: { type: String, enum: ['builtin', 'custom', 'dynamic'], default: 'custom' }, // القالب أو الدالة التي تنشئ المخرجات template: { type: String, default: '', maxlength: 50000 }, // المعاملات المقبولة parameters: [shortcodeParameterSchema], // إعدادات إضافية settings: { cacheEnabled: { type: Boolean, default: false }, cacheTTL: { type: Number, default: 3600 }, // seconds requiresAuth: { type: Boolean, default: false }, showInEditor: { type: Boolean, default: true }, icon: { type: String, default: 'bx-code-alt' }, category: { type: String, default: 'general' } }, // المخرجات (يمكن تخزينها مؤقتاً) cachedOutput: { html: { type: String }, expiresAt: { type: Date } }, // إحصائيات الاستخدام stats: { usageCount: { type: Number, default: 0 }, lastUsedAt: { type: Date }, avgRenderTime: { type: Number, default: 0 } }, isActive: { type: Boolean, default: true, index: true }, isDeleted: { type: Boolean, default: false, index: true }, createdAt: { type: Date, default: Date.now, immutable: true }, updatedAt: { type: Date, default: Date.now } }, { timestamps: true }); // ============================================ // Indexes // ============================================ storeShortcodeSchema.index({ userId: 1, name: 1 }, { unique: true }); storeShortcodeSchema.index({ userId: 1, isActive: 1 }); storeShortcodeSchema.index({ 'stats.usageCount': -1 }); storeShortcodeSchema.index({ 'settings.category': 1 }); // ============================================ // Static Methods - Shortcodes المدمجة // ============================================ /** * الحصول على قائمة الـ Shortcodes المدمجة في النظام * @returns {Array} - قائمة الـ Shortcodes المدمجة */ storeShortcodeSchema.statics.getBuiltinShortcodes = function() { return [ { name: 'products', displayName: 'Products Grid', description: 'Display a grid of products with filtering options', category: 'products', icon: 'bx-package', parameters: [ { name: 'limit', type: 'number', defaultValue: 12, description: 'Number of products to show' }, { name: 'type', type: 'string', defaultValue: 'all', description: 'Product type (digital, project, service, all)' }, { name: 'category', type: 'string', description: 'Filter by category/tag' }, { name: 'exclude', type: 'array', description: 'Product IDs to exclude (comma separated)' }, { name: 'layout', type: 'string', defaultValue: 'grid', description: 'Layout type (grid, list, carousel)' }, { name: 'columns', type: 'number', defaultValue: 4, description: 'Number of columns (2,3,4,6)' }, { name: 'sort', type: 'string', defaultValue: 'newest', description: 'Sort by (newest, popular, price_asc, price_desc)' } ] }, { name: 'product', displayName: 'Single Product', description: 'Display a single product by ID', category: 'products', icon: 'bx-package', parameters: [ { name: 'id', type: 'string', required: true, description: 'Product ID' }, { name: 'layout', type: 'string', defaultValue: 'default', description: 'Layout style (default, compact, detailed)' } ] }, { name: 'contact-form', displayName: 'Contact Form', description: 'Display a contact form for customers to message you', category: 'forms', icon: 'bx-envelope', parameters: [ { name: 'title', type: 'string', defaultValue: 'Contact Us', description: 'Form title' }, { name: 'fields', type: 'array', defaultValue: 'name,email,message', description: 'Form fields to show' } ] }, { name: 'store-info', displayName: 'Store Information', description: 'Display store information (name, description, followers, etc.)', category: 'info', icon: 'bx-store', parameters: [ { name: 'field', type: 'string', required: true, description: 'Field to display (name, description, logo, followers, rating)' } ] }, { name: 'cart-count', displayName: 'Cart Count', description: 'Display the number of items in cart', category: 'cart', icon: 'bx-cart', parameters: [] }, { name: 'categories', displayName: 'Categories List', description: 'Display all product categories', category: 'products', icon: 'bx-category', parameters: [ { name: 'showCount', type: 'boolean', defaultValue: true, description: 'Show product count per category' }, { name: 'layout', type: 'string', defaultValue: 'grid', description: 'Layout (grid, list, chips)' } ] }, { name: 'search-form', displayName: 'Search Form', description: 'Display a search form for products', category: 'forms', icon: 'bx-search', parameters: [ { name: 'placeholder', type: 'string', defaultValue: 'Search products...', description: 'Input placeholder' }, { name: 'showFilters', type: 'boolean', defaultValue: false, description: 'Show advanced filters' } ] }, { name: 'testimonials', displayName: 'Testimonials', description: 'Display customer testimonials', category: 'social', icon: 'bx-chat', parameters: [ { name: 'limit', type: 'number', defaultValue: 6, description: 'Number of testimonials to show' }, { name: 'autoplay', type: 'boolean', defaultValue: false, description: 'Enable carousel autoplay' } ] }, { name: 'social-links', displayName: 'Social Links', description: 'Display store social media links', category: 'social', icon: 'bx-share-alt', parameters: [ { name: 'layout', type: 'string', defaultValue: 'horizontal', description: 'Layout (horizontal, vertical)' } ] } ]; }; /** * تهيئة الـ Shortcodes المدمجة لمستخدم جديد * @param {string} userId - معرف المستخدم */ storeShortcodeSchema.statics.initializeBuiltins = async function(userId) { const builtins = this.getBuiltinShortcodes(); for (const shortcode of builtins) { await this.findOneAndUpdate( { userId, name: shortcode.name, type: 'builtin' }, { ...shortcode, userId, type: 'builtin', isActive: true }, { upsert: true } ); } }; // ============================================ // Instance Methods // ============================================ /** * تنفيذ الـ Shortcode وإرجاع HTML * @param {Object} attrs - المعاملات المُمررة * @param {Object} context - السياق (userId, storeId, etc.) * @returns {Promise} - HTML الناتج */ storeShortcodeSchema.methods.render = async function(attrs = {}, context = {}) { const startTime = Date.now(); try { // التحقق من التخزين المؤقت if (this.settings.cacheEnabled && this.cachedOutput && this.cachedOutput.expiresAt > new Date()) { return this.cachedOutput.html; } // دمج المعاملات مع القيم الافتراضية const mergedAttrs = {}; for (const param of this.parameters) { mergedAttrs[param.name] = attrs[param.name] !== undefined ? attrs[param.name] : param.defaultValue; } // تنفيذ القالب let output = ''; if (this.type === 'builtin') { output = await this.renderBuiltin(mergedAttrs, context); } else if (this.template) { output = await this.renderCustomTemplate(mergedAttrs, context); } // تحديث الإحصائيات this.stats.usageCount += 1; this.stats.lastUsedAt = new Date(); this.stats.avgRenderTime = (this.stats.avgRenderTime + (Date.now() - startTime)) / 2; // تحديث التخزين المؤقت if (this.settings.cacheEnabled) { this.cachedOutput = { html: output, expiresAt: new Date(Date.now() + this.settings.cacheTTL * 1000) }; } await this.save(); return output; } catch (error) { console.error(`Error rendering shortcode ${this.name}:`, error); return `
Error rendering ${this.displayName}: ${error.message}
`; } }; /** * تنفيذ Shortcode مدمج * @private */ storeShortcodeSchema.methods.renderBuiltin = async function(attrs, context) { const { storeId, userToken } = context; switch (this.name) { case 'products': return await renderProductsShortcode(attrs, storeId); case 'product': return await renderSingleProductShortcode(attrs, storeId); case 'contact-form': return renderContactFormShortcode(attrs, storeId); case 'store-info': return await renderStoreInfoShortcode(attrs, storeId); case 'cart-count': return `0`; case 'categories': return await renderCategoriesShortcode(attrs, storeId); case 'search-form': return renderSearchFormShortcode(attrs); case 'testimonials': return await renderTestimonialsShortcode(attrs, storeId); case 'social-links': return await renderSocialLinksShortcode(attrs, storeId); default: return `
Unknown shortcode: ${this.name}
`; } }; /** * تنفيذ قالب مخصص * @private */ storeShortcodeSchema.methods.renderCustomTemplate = async function(attrs, context) { let html = this.template; // استبدال المتغيرات في القالب for (const [key, value] of Object.entries(attrs)) { const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g'); html = html.replace(regex, String(value ?? '')); } return html; }; // ============================================ // دوال مساعدة لتنفيذ الـ Shortcodes المدمجة // ============================================ async function renderProductsShortcode(attrs, storeId) { const Product = mongoose.model('Product'); const StoreSettings = mongoose.model('StoreSettings'); const limit = Math.min(parseInt(attrs.limit) || 12, 48); const type = attrs.type !== 'all' ? attrs.type : null; const sort = attrs.sort || 'newest'; const layout = attrs.layout || 'grid'; const columns = parseInt(attrs.columns) || 4; let query = { userId: storeId, isActive: true }; if (type) query.type = type; if (attrs.category) query.tags = attrs.category; if (attrs.exclude) query._id = { $nin: attrs.exclude.split(',') }; let productsQuery = Product.find(query); switch (sort) { case 'popular': productsQuery = productsQuery.sort({ salesCount: -1 }); break; case 'price_asc': productsQuery = productsQuery.sort({ price: 1 }); break; case 'price_desc': productsQuery = productsQuery.sort({ price: -1 }); break; default: productsQuery = productsQuery.sort({ createdAt: -1 }); } const products = await productsQuery.limit(limit); const storeSettings = await StoreSettings.findOne({ userId: storeId }); if (!products.length) { return '
No products found
'; } const columnsClass = { 2: 'md:grid-cols-2', 3: 'md:grid-cols-3', 4: 'md:grid-cols-4', 6: 'md:grid-cols-6' }[columns] || 'md:grid-cols-4'; return `
${products.map(product => generateProductCardHtml(product, storeSettings)).join('')}
`; } async function renderSingleProductShortcode(attrs, storeId) { const Product = mongoose.model('Product'); const StoreSettings = mongoose.model('StoreSettings'); const product = await Product.findOne({ _id: attrs.id, userId: storeId, isActive: true }); if (!product) { return '
Product not found
'; } const storeSettings = await StoreSettings.findOne({ userId: storeId }); const layout = attrs.layout || 'default'; if (layout === 'compact') { return generateCompactProductHtml(product, storeSettings); } if (layout === 'detailed') { return generateDetailedProductHtml(product, storeSettings); } return generateProductCardHtml(product, storeSettings); } function generateProductCardHtml(product, storeSettings) { const currencySymbol = storeSettings?.currencySymbol || '$'; const primaryColor = storeSettings?.primaryColor || '#3b82f6'; const imageUrl = product.images?.[0] || '/assets/img/default-product.png'; return `
${escapeHtml(product.title)}

${escapeHtml(product.title)}

${escapeHtml(product.description?.substring(0, 80))}

${product.price} ${currencySymbol}
`; } function generateCompactProductHtml(product, storeSettings) { const currencySymbol = storeSettings?.currencySymbol || '$'; const imageUrl = product.images?.[0] || '/assets/img/default-product.png'; return `
${escapeHtml(product.title)}

${escapeHtml(product.title)}

${product.price} ${currencySymbol}
`; } function generateDetailedProductHtml(product, storeSettings) { const currencySymbol = storeSettings?.currencySymbol || '$'; const primaryColor = storeSettings?.primaryColor || '#3b82f6'; const imageUrl = product.images?.[0] || '/assets/img/default-product.png'; return `
${escapeHtml(product.title)} ${product.images && product.images.length > 1 ? `
${product.images.slice(1, 4).map(img => ` `).join('')}
` : ''}

${escapeHtml(product.title)}

${product.price} ${currencySymbol} ${product.type === 'digital' ? 'Digital' : ''} ${product.type === 'service' ? 'Service' : ''}

${escapeHtml(product.description)}

${product.deliveryTime ? `

Delivery: ${escapeHtml(product.deliveryTime)}

` : ''}
`; } async function renderStoreInfoShortcode(attrs, storeId) { const StoreSettings = mongoose.model('StoreSettings'); const User = mongoose.model('User'); const StoreFollow = mongoose.model('StoreFollow'); const field = attrs.field; const store = await User.findById(storeId); const settings = await StoreSettings.findOne({ userId: storeId }); switch (field) { case 'name': return settings?.storeName || store?.username || 'Store'; case 'description': return settings?.storeDescription || ''; case 'logo': return settings?.storeLogo ? `` : ''; case 'followers': const followers = await StoreFollow.countDocuments({ storeId }); return followers.toString(); case 'rating': const products = await mongoose.model('Product').find({ userId: storeId }); const avgRating = products.reduce((sum, p) => sum + (p.averageRating || 0), 0) / (products.length || 1); return avgRating.toFixed(1); default: return ''; } } function renderContactFormShortcode(attrs, storeId) { const title = attrs.title || 'Contact Us'; const fields = attrs.fields ? attrs.fields.split(',') : ['name', 'email', 'message']; const fieldsHtml = fields.map(field => { switch (field) { case 'name': return ``; case 'email': return ``; case 'message': return ``; default: return ''; } }).join(''); return `

${escapeHtml(title)}

${fieldsHtml}
`; } async function renderCategoriesShortcode(attrs, storeId) { const Product = mongoose.model('Product'); const categories = [ { name: 'Digital', type: 'digital', icon: 'bx-download' }, { name: 'Projects', type: 'project', icon: 'bx-code-alt' }, { name: 'Services', type: 'service', icon: 'bx-briefcase' } ]; const counts = await Promise.all( categories.map(async cat => ({ ...cat, count: await Product.countDocuments({ userId: storeId, type: cat.type, isActive: true }) })) ); const layout = attrs.layout || 'grid'; const showCount = attrs.showCount !== 'false'; if (layout === 'list') { return `
${counts.filter(c => c.count > 0).map(cat => ` ${cat.name} ${showCount ? `(${cat.count})` : ''} `).join('')}
`; } return `
${counts.filter(c => c.count > 0).map(cat => `

${cat.name}

${showCount ? `

${cat.count} products

` : ''}
`).join('')}
`; } function renderSearchFormShortcode(attrs) { const placeholder = attrs.placeholder || 'Search products...'; const showFilters = attrs.showFilters === 'true'; return `
${showFilters ? `
` : ''}
`; } async function renderTestimonialsShortcode(attrs, storeId) { // يمكن جلب التقييمات الفعلية من قاعدة البيانات const Review = mongoose.model('Review'); const reviews = await Review.find({ storeId, isApproved: true }) .sort({ createdAt: -1 }) .limit(Math.min(parseInt(attrs.limit) || 6, 12)); if (!reviews.length) { return '
No testimonials yet
'; } const autoplay = attrs.autoplay === 'true'; const carouselClass = autoplay ? 'testimonials-carousel' : ''; return `
${reviews.map(review => `
${Array(5).fill().map((_, i) => ` `).join('')}

"${escapeHtml(review.comment?.substring(0, 200))}"

${escapeHtml(review.author?.charAt(0) || 'U')}

${escapeHtml(review.author || 'Anonymous')}

Verified Buyer

`).join('')}
`; } async function renderSocialLinksShortcode(attrs, storeId) { const StoreSettings = mongoose.model('StoreSettings'); const settings = await StoreSettings.findOne({ userId: storeId }); const socialLinks = settings?.socialLinks || {}; const socialIcons = { instagram: 'bxl-instagram', facebook: 'bxl-facebook', twitter: 'bxl-twitter', tiktok: 'bxl-tiktok', linkedin: 'bxl-linkedin', github: 'bxl-github', youtube: 'bxl-youtube' }; const activeLinks = Object.entries(socialLinks) .filter(([key, value]) => value && socialIcons[key]); if (!activeLinks.length) return ''; const layout = attrs.layout || 'horizontal'; const flexDirection = layout === 'vertical' ? 'flex-col' : 'flex-row'; return ` `; } function escapeHtml(str) { if (!str) return ''; return String(str).replace(/[&<>]/g, function(m) { if (m === '&') return '&'; if (m === '<') return '<'; if (m === '>') return '>'; return m; }); } const StoreShortcode = mongoose.model('StoreShortcode', storeShortcodeSchema); module.exports = StoreShortcode;