| const mongoose = require('mongoose'); |
|
|
| |
| |
| |
| |
|
|
| const templateReviewSchema = new mongoose.Schema({ |
| userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, |
| rating: { type: Number, required: true, min: 1, max: 5 }, |
| comment: { type: String, maxlength: 500 }, |
| createdAt: { type: Date, default: Date.now } |
| }); |
|
|
| const templateVersionSchema = new mongoose.Schema({ |
| version: { type: String, required: true, match: /^\d+\.\d+\.\d+$/ }, |
| changelog: { type: String }, |
| zipUrl: { type: String }, |
| releasedAt: { type: Date, default: Date.now } |
| }); |
|
|
| const templateSchema = new mongoose.Schema({ |
| |
| |
| |
| name: { |
| type: String, |
| required: true, |
| trim: true, |
| maxlength: 100, |
| index: true |
| }, |
| slug: { |
| type: String, |
| required: true, |
| unique: true, |
| lowercase: true, |
| trim: true, |
| match: /^[a-z0-9]+(?:-[a-z0-9]+)*$/ |
| }, |
| description: { |
| type: String, |
| required: true, |
| maxlength: 500 |
| }, |
| |
| |
| |
| |
| category: { |
| type: String, |
| enum: ['fashion', 'electronics', 'food', 'art', 'beauty', 'sports', 'education', 'business', 'portfolio', 'blog', 'minimal', 'modern', 'dark', 'light'], |
| default: 'general', |
| index: true |
| }, |
| tags: [{ |
| type: String, |
| trim: true, |
| lowercase: true, |
| index: true |
| }], |
| |
| |
| |
| |
| previewImage: { |
| type: String, |
| required: true |
| }, |
| previewImages: [{ |
| type: String, |
| description: { type: String } |
| }], |
| previewVideo: { type: String }, |
| demoUrl: { type: String }, |
| thumbnail: { type: String }, |
| |
| |
| |
| |
| themeZipUrl: { |
| type: String, |
| required: true |
| }, |
| themeZipSize: { type: Number }, |
| themeZipHash: { type: String }, |
| |
| |
| |
| |
| themeData: { |
| |
| colors: { |
| primary: { type: String, default: '#3b82f6' }, |
| secondary: { type: String, default: '#8b5cf6' }, |
| accent: { type: String, default: '#f59e0b' }, |
| background: { type: String, default: '#ffffff' }, |
| text: { type: String, default: '#1f2937' }, |
| textLight: { type: String, default: '#6b7280' }, |
| border: { type: String, default: '#e5e7eb' }, |
| success: { type: String, default: '#10b981' }, |
| error: { type: String, default: '#ef4444' }, |
| warning: { type: String, default: '#f59e0b' } |
| }, |
| |
| |
| typography: { |
| fontFamily: { type: String, default: 'Inter, sans-serif' }, |
| headingFont: { type: String, default: 'Inter, sans-serif' }, |
| baseFontSize: { type: String, default: '16px' }, |
| enableGoogleFonts: { type: Boolean, default: true } |
| }, |
| |
| |
| layout: { |
| type: { type: String, enum: ['full-width', 'boxed', 'fluid'], default: 'full-width' }, |
| headerLayout: { type: String, enum: ['default', 'centered', 'minimal', 'transparent'], default: 'default' }, |
| footerLayout: { type: String, enum: ['default', 'minimal', 'columns'], default: 'default' }, |
| sidebarPosition: { type: String, enum: ['left', 'right', 'none'], default: 'none' }, |
| columns: { type: Number, default: 4, min: 2, max: 6 }, |
| stickyHeader: { type: Boolean, default: false }, |
| containerWidth: { type: String, default: '1280px' }, |
| containerPadding: { type: String, default: '20px' }, |
| showBreadcrumb: { type: Boolean, default: true }, |
| backToTop: { type: Boolean, default: true }, |
| showSearchBar: { type: Boolean, default: true }, |
| showCategories: { type: Boolean, default: true }, |
| productsPerRowMobile: { type: Number, default: 2 }, |
| productsPerRowTablet: { type: Number, default: 3 }, |
| enableStickyAddToCart: { type: Boolean, default: true }, |
| enableCompareProducts: { type: Boolean, default: false }, |
| enableWishlist: { type: Boolean, default: false }, |
| enableRecentlyViewed: { type: Boolean, default: true } |
| }, |
| |
| |
| borderRadius: { type: Number, default: 16, min: 0, max: 48 }, |
| shadowType: { type: String, enum: ['none', 'sm', 'md', 'lg', 'xl'], default: 'md' }, |
| animationType: { type: String, enum: ['none', 'lift', 'scale', 'glow'], default: 'lift' }, |
| glassEffect: { type: Boolean, default: false }, |
| |
| |
| backgroundType: { |
| type: String, |
| enum: ['default', 'dots', 'grid', 'cross', 'gradient', 'image'], |
| default: 'default' |
| }, |
| backgroundValue: { type: String, default: '' }, |
| |
| |
| customCss: { type: String, default: '' }, |
| customJs: { type: String, default: '' }, |
| |
| |
| sections: { type: mongoose.Schema.Types.Mixed, default: [] }, |
| |
| |
| storeInfo: { |
| storeName: { type: String, default: '' }, |
| storeLogo: { type: String, default: '' }, |
| storeBanner: { type: String, default: '' }, |
| storeDescription: { type: String, default: '' }, |
| contactEmail: { type: String, default: '' } |
| }, |
| |
| |
| pages: [{ |
| slug: { type: String, required: true }, |
| title: { type: String, required: true }, |
| content: { type: String, default: '' }, |
| css: { type: String, default: '' }, |
| js: { type: String, default: '' }, |
| seo: { |
| title: { type: String }, |
| description: { type: String }, |
| keywords: { type: String }, |
| ogImage: { type: String } |
| }, |
| isEnabled: { type: Boolean, default: true }, |
| order: { type: Number, default: 0 }, |
| type: { type: String, default: 'custom' } |
| }], |
| |
| |
| defaultPages: { |
| about: { |
| title: { type: String, default: 'About Us' }, |
| content: { type: String, default: '' }, |
| seo: { type: mongoose.Schema.Types.Mixed, default: {} } |
| }, |
| contact: { |
| title: { type: String, default: 'Contact Us' }, |
| content: { type: String, default: '' }, |
| seo: { type: mongoose.Schema.Types.Mixed, default: {} } |
| } |
| }, |
| |
| |
| socialLinks: { |
| instagram: { type: String, default: '' }, |
| facebook: { type: String, default: '' }, |
| twitter: { type: String, default: '' }, |
| tiktok: { type: String, default: '' }, |
| linkedin: { type: String, default: '' }, |
| youtube: { type: String, default: '' }, |
| whatsapp: { type: String, default: '' }, |
| telegram: { type: String, default: '' } |
| }, |
| |
| |
| paymentSettings: { |
| enableCOD: { type: Boolean, default: false }, |
| enableInstallments: { type: Boolean, default: false }, |
| minOrderAmount: { type: Number, default: 0 }, |
| freeShippingThreshold: { type: Number, default: 0 }, |
| taxRate: { type: Number, default: 0, min: 0, max: 100 }, |
| shippingCost: { type: Number, default: 0 } |
| }, |
| |
| |
| productsSettings: { |
| productViewType: { type: String, enum: ['modal', 'page'], default: 'modal' }, |
| productsPerRow: { type: Number, default: 4, min: 2, max: 6 }, |
| enableQuickView: { type: Boolean, default: true }, |
| enableProductReviews: { type: Boolean, default: true }, |
| autoApproveReviews: { type: Boolean, default: false } |
| }, |
| |
| |
| customHeaderFooter: { |
| header: { type: String, default: '' }, |
| footer: { type: String, default: '' }, |
| sidebar: { type: String, default: '' }, |
| productCard: { type: String, default: '' }, |
| categoryCard: { type: String, default: '' }, |
| css: { type: String, default: '' }, |
| js: { type: String, default: '' } |
| }, |
| |
| templateUrl: { type: String, default: null }, |
| uploadedFiles: { |
| type: mongoose.Schema.Types.Mixed, |
| default: null |
| }, |
| hasIndexHtml: { type: Boolean, default: false }, |
|
|
| |
| currency: { type: String, default: 'USD', uppercase: true }, |
| |
| |
| pagesOrder: { |
| type: mongoose.Schema.Types.Mixed, |
| default: { products: 1, about: 2, contact: 3 } |
| }, |
| |
| |
| storeCoupons: [{ |
| code: { type: String, uppercase: true }, |
| discount: { type: Number, min: 0, max: 100 }, |
| discountType: { type: String, enum: ['percentage', 'fixed'], default: 'percentage' }, |
| minPurchase: { type: Number, default: 0 }, |
| maxDiscount: { type: Number }, |
| validUntil: { type: Date }, |
| isActive: { type: Boolean, default: true } |
| }], |
| |
| |
| shippingZones: [{ |
| name: { type: String }, |
| countries: [{ type: String }], |
| cost: { type: Number, default: 0 }, |
| freeShippingAbove: { type: Number, default: 0 }, |
| estimatedDays: { type: String, default: '3-7 business days' } |
| }], |
| |
| |
| seoSettings: { |
| metaTitleTemplate: { type: String, default: '{{title}} | {{storeName}}' }, |
| metaDescriptionTemplate: { type: String, default: '' }, |
| enableBreadcrumbs: { type: Boolean, default: true }, |
| sitemapEnabled: { type: Boolean, default: true } |
| }, |
| |
| |
| performanceSettings: { |
| enableLazyLoad: { type: Boolean, default: true }, |
| enableImageOptimization: { type: Boolean, default: true }, |
| enablePreload: { type: Boolean, default: true }, |
| cacheTTL: { type: Number, default: 3600 } |
| }, |
| |
| |
| templateMeta: { |
| minVersion: { type: String, default: '1.0.0' }, |
| maxVersion: { type: String, default: '3.0.0' }, |
| requires: [{ type: String }], |
| screenshots: [{ type: String }], |
| demoUrl: { type: String }, |
| documentationUrl: { type: String }, |
| supportUrl: { type: String } |
| } |
| }, |
| |
| |
| |
| |
| requiredSubscription: { |
| type: String, |
| enum: ['free', 'basic', 'pro', 'business', 'enterprise'], |
| default: 'free', |
| index: true |
| }, |
| requiredPermissions: [{ |
| type: String, |
| enum: ['store_enabled', 'custom_domain', 'analytics'] |
| }], |
| |
| |
| |
| |
| downloadsCount: { |
| type: Number, |
| default: 0, |
| index: true |
| }, |
| viewsCount: { |
| type: Number, |
| default: 0 |
| }, |
| rating: { |
| type: Number, |
| default: 0, |
| min: 0, |
| max: 5 |
| }, |
| ratingCount: { |
| type: Number, |
| default: 0 |
| }, |
| reviews: [templateReviewSchema], |
| |
| |
| |
| |
| version: { |
| type: String, |
| default: '1.0.0', |
| match: /^\d+\.\d+\.\d+$/ |
| }, |
| versions: [templateVersionSchema], |
| |
| |
| |
| |
| isActive: { |
| type: Boolean, |
| default: true, |
| index: true |
| }, |
| isFeatured: { |
| type: Boolean, |
| default: false, |
| index: true |
| }, |
| isVerified: { |
| type: Boolean, |
| default: false, |
| index: true |
| }, |
| |
| |
| |
| |
| authorId: { |
| type: mongoose.Schema.Types.ObjectId, |
| ref: 'User', |
| index: true |
| }, |
| authorName: { type: String }, |
| authorEmail: { type: String }, |
| authorWebsite: { type: String }, |
| |
| |
| |
| |
| settings: { |
| requiresSetup: { type: Boolean, default: false }, |
| setupInstructions: { type: String }, |
| compatibleWith: [{ |
| platformVersion: { type: String }, |
| minVersion: { type: String } |
| }], |
| dependencies: [{ type: String }] |
| }, |
| |
| |
| |
| |
| installedBy: [{ |
| userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, |
| installedAt: { type: Date, default: Date.now }, |
| storeId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, |
| appliedSettings: { type: mongoose.Schema.Types.Mixed } |
| }], |
| |
| createdAt: { type: Date, default: Date.now, immutable: true }, |
| updatedAt: { type: Date, default: Date.now } |
| }, { |
| timestamps: true, |
| toJSON: { virtuals: true }, |
| toObject: { virtuals: true } |
| }); |
|
|
| |
| |
| |
| templateSchema.index({ category: 1, isActive: 1, isFeatured: -1 }); |
| templateSchema.index({ downloadsCount: -1, rating: -1 }); |
| templateSchema.index({ tags: 1 }); |
| templateSchema.index({ name: 'text', description: 'text', tags: 'text' }); |
| templateSchema.index({ requiredSubscription: 1 }); |
| templateSchema.index({ 'installedBy.userId': 1 }); |
| templateSchema.index({ 'themeData.currency': 1 }); |
| templateSchema.index({ 'themeData.layout.type': 1 }); |
| templateSchema.index({ version: -1 }); |
|
|
| |
| |
| |
| templateSchema.virtual('downloadUrl').get(function() { |
| return `/api/templates/${this.slug}/download`; |
| }); |
|
|
| templateSchema.virtual('previewUrl').get(function() { |
| return `/api/templates/${this.slug}/preview`; |
| }); |
|
|
| templateSchema.virtual('isFree').get(function() { |
| return this.requiredSubscription === 'free'; |
| }); |
|
|
| templateSchema.virtual('totalInstalls').get(function() { |
| return this.installedBy?.length || 0; |
| }); |
|
|
| templateSchema.virtual('lastInstalledAt').get(function() { |
| if (!this.installedBy || this.installedBy.length === 0) return null; |
| return this.installedBy[this.installedBy.length - 1]?.installedAt; |
| }); |
|
|
| |
| |
| |
| templateSchema.pre('save', function(next) { |
| this.updatedAt = Date.now(); |
| |
| |
| if (!this.slug && this.name) { |
| this.slug = this.name |
| .toLowerCase() |
| .replace(/[^a-z0-9]+/g, '-') |
| .replace(/^-|-$/g, ''); |
| } |
| |
| |
| if (this.previewImages && this.previewImages.length > 0) { |
| this.previewImages = this.previewImages.filter(img => img && img.trim() !== ''); |
| } |
| |
| |
| if (this.reviews && this.reviews.length > 0) { |
| const totalRating = this.reviews.reduce((sum, r) => sum + r.rating, 0); |
| this.rating = totalRating / this.reviews.length; |
| this.ratingCount = this.reviews.length; |
| } |
| |
| next(); |
| }); |
|
|
| |
| |
| |
|
|
| |
| |
| |
| templateSchema.statics.getFeatured = async function(limit = 6) { |
| return this.find({ isActive: true, isFeatured: true }) |
| .sort({ downloadsCount: -1, rating: -1 }) |
| .limit(limit) |
| .select('name slug category previewImage downloadsCount rating requiredSubscription themeData.storeInfo'); |
| }; |
|
|
| |
| |
| |
| templateSchema.statics.getMostDownloaded = async function(limit = 12) { |
| return this.find({ isActive: true }) |
| .sort({ downloadsCount: -1 }) |
| .limit(limit) |
| .select('name slug category previewImage downloadsCount rating requiredSubscription'); |
| }; |
|
|
| |
| |
| |
| templateSchema.statics.getByCategory = async function(category, limit = 20) { |
| return this.find({ isActive: true, category }) |
| .sort({ downloadsCount: -1 }) |
| .limit(limit); |
| }; |
|
|
| |
| |
| |
| templateSchema.statics.searchTemplates = async function(query, filters = {}) { |
| const searchQuery = { isActive: true }; |
| |
| if (query) { |
| searchQuery.$text = { $search: query }; |
| } |
| |
| if (filters.category && filters.category !== 'all') { |
| searchQuery.category = filters.category; |
| } |
| |
| if (filters.requiredSubscription && filters.requiredSubscription !== 'all') { |
| searchQuery.requiredSubscription = filters.requiredSubscription; |
| } |
| |
| if (filters.minRating) { |
| searchQuery.rating = { $gte: parseFloat(filters.minRating) }; |
| } |
| |
| let sort = {}; |
| switch (filters.sort) { |
| case 'popular': sort = { downloadsCount: -1 }; break; |
| case 'rating': sort = { rating: -1 }; break; |
| case 'newest': sort = { createdAt: -1 }; break; |
| default: sort = { downloadsCount: -1 }; |
| } |
| |
| return this.find(searchQuery) |
| .sort(sort) |
| .limit(filters.limit || 20) |
| .skip(filters.offset || 0); |
| }; |
|
|
| |
| |
| |
| templateSchema.statics.incrementViews = async function(templateId) { |
| return this.findByIdAndUpdate(templateId, { $inc: { viewsCount: 1 } }); |
| }; |
|
|
| |
| |
| |
| templateSchema.statics.recordDownload = async function(templateId, userId, storeId, appliedSettings = {}) { |
| const template = await this.findById(templateId); |
| if (!template) throw new Error('Template not found'); |
| |
| template.downloadsCount += 1; |
| |
| |
| template.installedBy.push({ |
| userId, |
| storeId, |
| installedAt: new Date(), |
| appliedSettings |
| }); |
| |
| |
| if (template.installedBy.length > 100) { |
| template.installedBy = template.installedBy.slice(-100); |
| } |
| |
| await template.save(); |
| return template; |
| }; |
|
|
| |
| |
| |
| templateSchema.statics.addReview = async function(templateId, userId, rating, comment) { |
| const template = await this.findById(templateId); |
| if (!template) throw new Error('Template not found'); |
| |
| const existingReview = template.reviews.find(r => r.userId.toString() === userId); |
| if (existingReview) { |
| existingReview.rating = rating; |
| existingReview.comment = comment; |
| existingReview.createdAt = new Date(); |
| } else { |
| template.reviews.push({ userId, rating, comment }); |
| } |
| |
| await template.save(); |
| return template; |
| }; |
|
|
| |
| |
| |
| templateSchema.statics.getSimilarTemplates = async function(templateId, limit = 4) { |
| const template = await this.findById(templateId); |
| if (!template) return []; |
| |
| return this.find({ |
| isActive: true, |
| _id: { $ne: templateId }, |
| $or: [ |
| { category: template.category }, |
| { tags: { $in: template.tags } } |
| ] |
| }) |
| .sort({ downloadsCount: -1, rating: -1 }) |
| .limit(limit); |
| }; |
|
|
| |
| |
| |
| templateSchema.statics.updateAfterApply = async function(templateId, userId, success = true) { |
| if (!success) return; |
| |
| const template = await this.findById(templateId); |
| if (template) { |
| template.downloadsCount += 1; |
| |
| |
| const existingInstall = template.installedBy.find(i => i.userId.toString() === userId); |
| if (existingInstall) { |
| existingInstall.installedAt = new Date(); |
| } else { |
| template.installedBy.push({ userId, storeId: userId, installedAt: new Date() }); |
| } |
| |
| await template.save(); |
| } |
| }; |
|
|
| const TemplateStore = mongoose.model('TemplateStore', templateSchema); |
|
|
| module.exports = TemplateStore; |