Mark-Lasfar
feat(seo): add complete dynamic sitemap system with posts, jobs, templates, pages, tags, images, and comments
bee235c | 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<string>} - 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 `<div class="shortcode-error">Error rendering ${this.displayName}: ${error.message}</div>`; | |
| } | |
| }; | |
| /** | |
| * تنفيذ 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 `<span class="cart-count" data-user-token="${userToken || ''}">0</span>`; | |
| 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 `<div class="shortcode-unknown">Unknown shortcode: ${this.name}</div>`; | |
| } | |
| }; | |
| /** | |
| * تنفيذ قالب مخصص | |
| * @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 '<div class="text-center py-8 text-gray-500">No products found</div>'; | |
| } | |
| 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 ` | |
| <div class="shortcode-products products-${layout} grid grid-cols-1 sm:grid-cols-2 ${columnsClass} gap-6"> | |
| ${products.map(product => generateProductCardHtml(product, storeSettings)).join('')} | |
| </div> | |
| `; | |
| } | |
| 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 '<div class="text-center py-4 text-gray-500">Product not found</div>'; | |
| } | |
| 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 ` | |
| <div class="product-card bg-white dark:bg-gray-800 rounded-xl overflow-hidden shadow-md hover:shadow-xl transition-all duration-300"> | |
| <img src="${imageUrl}" class="w-full h-48 object-cover" alt="${escapeHtml(product.title)}" loading="lazy"> | |
| <div class="p-4"> | |
| <h3 class="font-semibold text-lg line-clamp-1">${escapeHtml(product.title)}</h3> | |
| <p class="text-sm text-gray-500 mt-1 line-clamp-2">${escapeHtml(product.description?.substring(0, 80))}</p> | |
| <div class="flex justify-between items-center mt-3"> | |
| <span class="text-xl font-bold" style="color: ${primaryColor}">${product.price} ${currencySymbol}</span> | |
| <button onclick="addToCart('${product._id}')" | |
| class="px-4 py-2 text-white rounded-lg text-sm transition-all hover:opacity-90" | |
| style="background-color: ${primaryColor}"> | |
| Add to Cart | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function generateCompactProductHtml(product, storeSettings) { | |
| const currencySymbol = storeSettings?.currencySymbol || '$'; | |
| const imageUrl = product.images?.[0] || '/assets/img/default-product.png'; | |
| return ` | |
| <div class="compact-product flex items-center gap-4 p-3 bg-white dark:bg-gray-800 rounded-lg shadow-sm hover:shadow-md transition"> | |
| <img src="${imageUrl}" class="w-16 h-16 object-cover rounded" alt="${escapeHtml(product.title)}"> | |
| <div class="flex-1"> | |
| <h4 class="font-medium">${escapeHtml(product.title)}</h4> | |
| <span class="text-lg font-bold text-blue-600">${product.price} ${currencySymbol}</span> | |
| </div> | |
| <button onclick="addToCart('${product._id}')" class="px-3 py-1 bg-blue-500 text-white rounded-lg text-sm"> | |
| Buy | |
| </button> | |
| </div> | |
| `; | |
| } | |
| function generateDetailedProductHtml(product, storeSettings) { | |
| const currencySymbol = storeSettings?.currencySymbol || '$'; | |
| const primaryColor = storeSettings?.primaryColor || '#3b82f6'; | |
| const imageUrl = product.images?.[0] || '/assets/img/default-product.png'; | |
| return ` | |
| <div class="detailed-product flex flex-col md:flex-row gap-6 p-6 bg-white dark:bg-gray-800 rounded-xl shadow-lg"> | |
| <div class="md:w-1/3"> | |
| <img src="${imageUrl}" class="w-full rounded-lg object-cover" alt="${escapeHtml(product.title)}"> | |
| ${product.images && product.images.length > 1 ? ` | |
| <div class="flex gap-2 mt-3"> | |
| ${product.images.slice(1, 4).map(img => ` | |
| <img src="${img}" class="w-16 h-16 rounded cursor-pointer hover:opacity-80" onclick="this.parentElement.parentElement.querySelector('img:first-child').src='${img}'"> | |
| `).join('')} | |
| </div> | |
| ` : ''} | |
| </div> | |
| <div class="md:w-2/3"> | |
| <h2 class="text-2xl font-bold">${escapeHtml(product.title)}</h2> | |
| <div class="flex items-center gap-2 mt-2"> | |
| <span class="text-3xl font-bold" style="color: ${primaryColor}">${product.price} ${currencySymbol}</span> | |
| ${product.type === 'digital' ? '<span class="px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded-full">Digital</span>' : ''} | |
| ${product.type === 'service' ? '<span class="px-2 py-1 text-xs bg-purple-100 text-purple-700 rounded-full">Service</span>' : ''} | |
| </div> | |
| <p class="text-gray-600 dark:text-gray-400 mt-4 leading-relaxed">${escapeHtml(product.description)}</p> | |
| ${product.deliveryTime ? ` | |
| <p class="mt-3 text-sm text-gray-500"><i class="bx bx-time"></i> Delivery: ${escapeHtml(product.deliveryTime)}</p> | |
| ` : ''} | |
| <button onclick="addToCart('${product._id}')" | |
| class="mt-6 px-6 py-3 text-white rounded-lg font-semibold transition-all hover:opacity-90" | |
| style="background-color: ${primaryColor}"> | |
| <i class="bx bx-cart-add"></i> Add to Cart | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| 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 ? `<img src="${settings.storeLogo}" alt="Store Logo" class="store-logo">` : ''; | |
| 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 `<input type="text" name="name" placeholder="Your Name" class="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500" required>`; | |
| case 'email': | |
| return `<input type="email" name="email" placeholder="Your Email" class="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500" required>`; | |
| case 'message': | |
| return `<textarea name="message" rows="4" placeholder="Your Message" class="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500" required></textarea>`; | |
| default: | |
| return ''; | |
| } | |
| }).join(''); | |
| return ` | |
| <div class="shortcode-contact-form bg-white dark:bg-gray-800 rounded-xl shadow-md p-6"> | |
| <h3 class="text-xl font-bold mb-4">${escapeHtml(title)}</h3> | |
| <form onsubmit="event.preventDefault(); sendStoreContactMessage('${storeId}')" class="space-y-4"> | |
| ${fieldsHtml} | |
| <button type="submit" class="w-full py-3 bg-blue-500 text-white rounded-lg font-semibold hover:bg-blue-600 transition"> | |
| Send Message | |
| </button> | |
| </form> | |
| <div id="contact-form-response" class="mt-3 text-center hidden"></div> | |
| </div> | |
| `; | |
| } | |
| 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 ` | |
| <div class="categories-list space-y-2"> | |
| ${counts.filter(c => c.count > 0).map(cat => ` | |
| <a href="#" onclick="filterProductsByCategory('${cat.type}'); return false;" | |
| class="flex justify-between items-center p-3 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition"> | |
| <span><i class='bx ${cat.icon} mr-2'></i> ${cat.name}</span> | |
| ${showCount ? `<span class="text-sm text-gray-500">(${cat.count})</span>` : ''} | |
| </a> | |
| `).join('')} | |
| </div> | |
| `; | |
| } | |
| return ` | |
| <div class="categories-grid grid grid-cols-2 md:grid-cols-3 gap-4"> | |
| ${counts.filter(c => c.count > 0).map(cat => ` | |
| <a href="#" onclick="filterProductsByCategory('${cat.type}'); return false;" | |
| class="category-card p-4 bg-white dark:bg-gray-800 rounded-lg text-center hover:shadow-md transition"> | |
| <i class='bx ${cat.icon} text-3xl text-blue-500'></i> | |
| <h3 class="font-medium mt-2">${cat.name}</h3> | |
| ${showCount ? `<p class="text-xs text-gray-500">${cat.count} products</p>` : ''} | |
| </a> | |
| `).join('')} | |
| </div> | |
| `; | |
| } | |
| function renderSearchFormShortcode(attrs) { | |
| const placeholder = attrs.placeholder || 'Search products...'; | |
| const showFilters = attrs.showFilters === 'true'; | |
| return ` | |
| <div class="shortcode-search-form"> | |
| <form onsubmit="event.preventDefault(); searchProducts(this.querySelector('input').value)" class="relative"> | |
| <input type="text" placeholder="${escapeHtml(placeholder)}" | |
| class="w-full p-3 pl-12 border rounded-lg focus:ring-2 focus:ring-blue-500"> | |
| <i class='bx bx-search absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 text-xl'></i> | |
| </form> | |
| ${showFilters ? ` | |
| <div class="flex gap-2 mt-3 flex-wrap"> | |
| <button onclick="filterProductsByCategory('digital')" class="px-3 py-1 text-sm bg-gray-100 rounded-full">Digital</button> | |
| <button onclick="filterProductsByCategory('project')" class="px-3 py-1 text-sm bg-gray-100 rounded-full">Projects</button> | |
| <button onclick="filterProductsByCategory('service')" class="px-3 py-1 text-sm bg-gray-100 rounded-full">Services</button> | |
| </div> | |
| ` : ''} | |
| </div> | |
| `; | |
| } | |
| 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 '<div class="text-center py-8 text-gray-500">No testimonials yet</div>'; | |
| } | |
| const autoplay = attrs.autoplay === 'true'; | |
| const carouselClass = autoplay ? 'testimonials-carousel' : ''; | |
| return ` | |
| <div class="testimonials-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 ${carouselClass}"> | |
| ${reviews.map(review => ` | |
| <div class="testimonial-card p-6 bg-white dark:bg-gray-800 rounded-xl shadow-md"> | |
| <div class="flex gap-1 mb-3"> | |
| ${Array(5).fill().map((_, i) => ` | |
| <i class='bx ${i < review.rating ? 'bxs-star' : 'bx-star'} text-yellow-400'></i> | |
| `).join('')} | |
| </div> | |
| <p class="text-gray-600 dark:text-gray-400 mb-4">"${escapeHtml(review.comment?.substring(0, 200))}"</p> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center"> | |
| <span class="text-white font-bold">${escapeHtml(review.author?.charAt(0) || 'U')}</span> | |
| </div> | |
| <div> | |
| <p class="font-semibold">${escapeHtml(review.author || 'Anonymous')}</p> | |
| <p class="text-xs text-gray-500">Verified Buyer</p> | |
| </div> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| } | |
| 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 ` | |
| <div class="social-links flex ${flexDirection} gap-4 flex-wrap"> | |
| ${activeLinks.map(([platform, url]) => ` | |
| <a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" | |
| class="social-link text-gray-600 hover:text-blue-500 transition transform hover:scale-110"> | |
| <i class='bx ${socialIcons[platform]} text-2xl'></i> | |
| </a> | |
| `).join('')} | |
| </div> | |
| `; | |
| } | |
| 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; |