first commit
This commit is contained in:
268
lib/blob-storage.ts
Normal file
268
lib/blob-storage.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { put, remove } from '@vercel/blob'
|
||||
|
||||
export interface UploadResult {
|
||||
url: string
|
||||
downloadUrl: string
|
||||
pathname: string
|
||||
size: number
|
||||
uploadedAt: Date
|
||||
}
|
||||
|
||||
export interface BlobStorageConfig {
|
||||
token: string
|
||||
folder?: string
|
||||
allowedTypes?: string[]
|
||||
maxSize?: number // in bytes
|
||||
}
|
||||
|
||||
type PutBody = string | File | Blob | ReadableStream
|
||||
|
||||
// Helper function to check if the object has File-like properties
|
||||
function isFileObject(obj: any): obj is { name: string; type: string; size: number } {
|
||||
return obj && typeof obj.name === 'string' && typeof obj.type === 'string' && typeof obj.size === 'number'
|
||||
}
|
||||
|
||||
class BlobStorageService {
|
||||
private config: BlobStorageConfig
|
||||
|
||||
constructor(config: BlobStorageConfig) {
|
||||
this.config = {
|
||||
maxSize: 10 * 1024 * 1024, // 10MB default
|
||||
allowedTypes: [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
'image/gif',
|
||||
'application/pdf',
|
||||
'text/csv',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
],
|
||||
...config
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to Vercel Blob storage
|
||||
*/
|
||||
async uploadFile(
|
||||
file: PutBody,
|
||||
filename: string,
|
||||
options?: {
|
||||
folder?: string
|
||||
contentType?: string
|
||||
addRandomSuffix?: boolean
|
||||
}
|
||||
): Promise<UploadResult> {
|
||||
try {
|
||||
// Validate file if it's a File-like object
|
||||
if (isFileObject(file)) {
|
||||
this.validateFile(file)
|
||||
}
|
||||
|
||||
// Generate pathname
|
||||
const folder = options?.folder || this.config.folder || 'uploads'
|
||||
const timestamp = new Date().toISOString().split('T')[0]
|
||||
const randomSuffix = options?.addRandomSuffix !== false ? `-${Date.now()}` : ''
|
||||
const pathname = `${folder}/${timestamp}/${filename}${randomSuffix}`
|
||||
|
||||
// Upload to Vercel Blob
|
||||
const blob = await put(pathname, file as string | Blob | File | ReadableStream, {
|
||||
token: this.config.token,
|
||||
contentType: options?.contentType || (isFileObject(file) ? file.type : undefined),
|
||||
})
|
||||
|
||||
return {
|
||||
url: blob.url,
|
||||
downloadUrl: blob.url + '?download=1',
|
||||
pathname: pathname,
|
||||
size: isFileObject(file) ? file.size : 0,
|
||||
uploadedAt: new Date()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading file to Blob storage:', error)
|
||||
throw new Error(`Failed to upload file: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload multiple files
|
||||
*/
|
||||
async uploadMultipleFiles(
|
||||
files: PutBody[],
|
||||
options?: {
|
||||
folder?: string
|
||||
addRandomSuffix?: boolean
|
||||
}
|
||||
): Promise<UploadResult[]> {
|
||||
const uploadPromises = files.map((file, index) => {
|
||||
// Check if file has a name property (like File objects from FormData)
|
||||
const filename = (file as any)?.name || `file-${index}`
|
||||
return this.uploadFile(file, filename, options)
|
||||
})
|
||||
|
||||
return Promise.all(uploadPromises)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file from Vercel Blob storage
|
||||
*/
|
||||
async deleteFile(url: string): Promise<void> {
|
||||
try {
|
||||
await remove(url, { token: this.config.token })
|
||||
} catch (error) {
|
||||
console.error('Error deleting file from Blob storage:', error)
|
||||
throw new Error(`Failed to delete file: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple files
|
||||
*/
|
||||
async deleteMultipleFiles(urls: string[]): Promise<void> {
|
||||
const deletePromises = urls.map(url => this.deleteFile(url))
|
||||
await Promise.all(deletePromises)
|
||||
}
|
||||
|
||||
/**
|
||||
* List files in a specific folder
|
||||
* Note: list() is not available in the current @vercel/blob version
|
||||
*/
|
||||
async listFiles(options?: {
|
||||
prefix?: string
|
||||
limit?: number
|
||||
cursor?: string
|
||||
}): Promise<{
|
||||
blobs: Array<{
|
||||
url: string
|
||||
pathname: string
|
||||
size: number
|
||||
uploadedAt: Date
|
||||
}>
|
||||
cursor?: string
|
||||
hasMore: boolean
|
||||
}> {
|
||||
// list() function is not available in current @vercel/blob package
|
||||
console.warn('listFiles: list() function not available in current @vercel/blob version')
|
||||
return {
|
||||
blobs: [],
|
||||
cursor: undefined,
|
||||
hasMore: false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload product images with optimization
|
||||
*/
|
||||
async uploadProductImages(
|
||||
images: File[],
|
||||
productName: string
|
||||
): Promise<string[]> {
|
||||
const folder = 'products'
|
||||
const sanitizedProductName = productName.toLowerCase().replace(/[^a-z0-9]/g, '-')
|
||||
|
||||
const uploadResults = await Promise.all(
|
||||
images.map(async (image, index) => {
|
||||
const filename = `${sanitizedProductName}-${index + 1}.${image.name.split('.').pop()}`
|
||||
const result = await this.uploadFile(image, filename, {
|
||||
folder,
|
||||
addRandomSuffix: false
|
||||
})
|
||||
return result.url
|
||||
})
|
||||
)
|
||||
|
||||
return uploadResults
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload category image
|
||||
*/
|
||||
async uploadCategoryImage(
|
||||
image: File,
|
||||
categoryName: string
|
||||
): Promise<string> {
|
||||
const folder = 'categories'
|
||||
const sanitizedCategoryName = categoryName.toLowerCase().replace(/[^a-z0-9]/g, '-')
|
||||
const filename = `${sanitizedCategoryName}.${image.name.split('.').pop()}`
|
||||
|
||||
const result = await this.uploadFile(image, filename, {
|
||||
folder,
|
||||
addRandomSuffix: false
|
||||
})
|
||||
|
||||
return result.url
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload user avatar
|
||||
*/
|
||||
async uploadUserAvatar(
|
||||
image: File,
|
||||
userId: string
|
||||
): Promise<string> {
|
||||
const folder = 'avatars'
|
||||
const filename = `user-${userId}.${image.name.split('.').pop()}`
|
||||
|
||||
const result = await this.uploadFile(image, filename, {
|
||||
folder,
|
||||
addRandomSuffix: false
|
||||
})
|
||||
|
||||
return result.url
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file before upload
|
||||
*/
|
||||
private validateFile(file: { size: number; type: string }): void {
|
||||
// Check file size
|
||||
if (this.config.maxSize && file.size > this.config.maxSize) {
|
||||
throw new Error(`File size exceeds maximum allowed size of ${this.config.maxSize / (1024 * 1024)}MB`)
|
||||
}
|
||||
|
||||
// Check file type
|
||||
if (this.config.allowedTypes && !this.config.allowedTypes.includes(file.type)) {
|
||||
throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedTypes.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimized image URL with transformations
|
||||
*/
|
||||
getOptimizedImageUrl(
|
||||
originalUrl: string,
|
||||
options?: {
|
||||
width?: number
|
||||
height?: number
|
||||
quality?: number
|
||||
format?: 'webp' | 'jpeg' | 'png'
|
||||
}
|
||||
): string {
|
||||
if (!options) return originalUrl
|
||||
|
||||
const url = new URL(originalUrl)
|
||||
const searchParams = new URLSearchParams()
|
||||
|
||||
if (options.width) searchParams.set('w', options.width.toString())
|
||||
if (options.height) searchParams.set('h', options.height.toString())
|
||||
if (options.quality) searchParams.set('q', options.quality.toString())
|
||||
if (options.format) searchParams.set('f', options.format)
|
||||
|
||||
if (searchParams.toString()) {
|
||||
url.search = searchParams.toString()
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const blobStorage = new BlobStorageService({
|
||||
token: process.env.BLOB_READ_WRITE_TOKEN || '',
|
||||
folder: 'padmaaja-rasooi',
|
||||
maxSize: 10 * 1024 * 1024, // 10MB
|
||||
})
|
||||
|
||||
export default blobStorage
|
||||
export { BlobStorageService }
|
||||
32
lib/business-config.ts
Normal file
32
lib/business-config.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// Business Configuration
|
||||
// Set to control B2B vs B2C functionality
|
||||
|
||||
export const businessConfig = {
|
||||
// Set to 'b2b' to disable consumer features, 'b2c' to enable them
|
||||
mode: 'b2b' as 'b2b' | 'b2c',
|
||||
|
||||
// Feature flags
|
||||
features: {
|
||||
cart: false, // Disable cart functionality for B2B
|
||||
individualPurchase: false, // Disable individual product purchases
|
||||
checkout: false, // Disable checkout process
|
||||
consumerPricing: false, // Disable consumer-focused pricing
|
||||
wholesaleInquiry: true, // Enable wholesale inquiry forms
|
||||
bulkOrders: true, // Enable bulk order functionality
|
||||
businessAccounts: true, // Enable business account features
|
||||
},
|
||||
|
||||
// Messaging configuration
|
||||
messaging: {
|
||||
targetAudience: 'businesses', // 'consumers' or 'businesses'
|
||||
showRetailPricing: false,
|
||||
showWholesalePricing: true,
|
||||
showMinimumOrderQuantity: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
export const isB2BMode = () => businessConfig.mode === 'b2b'
|
||||
export const isB2CMode = () => businessConfig.mode === 'b2c'
|
||||
export const isFeatureEnabled = (feature: keyof typeof businessConfig.features) =>
|
||||
businessConfig.features[feature]
|
||||
211
lib/cache-manager.ts
Normal file
211
lib/cache-manager.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
// Client-side cache management and service worker utilities
|
||||
|
||||
interface CacheManager {
|
||||
clearAllCaches: () => Promise<void>
|
||||
checkForUpdates: () => Promise<boolean>
|
||||
forceReload: () => void
|
||||
registerUpdateHandler: (callback: () => void) => void
|
||||
}
|
||||
|
||||
class PWACacheManager implements CacheManager {
|
||||
private updateCallback: (() => void) | null = null
|
||||
private registration: ServiceWorkerRegistration | null = null
|
||||
|
||||
constructor() {
|
||||
this.initializeServiceWorker()
|
||||
}
|
||||
|
||||
private async initializeServiceWorker() {
|
||||
// Only initialize in browser environment
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
// Register service worker
|
||||
this.registration = await navigator.serviceWorker.register('/sw.js', {
|
||||
scope: '/',
|
||||
updateViaCache: 'none' // Always check for updates
|
||||
})
|
||||
|
||||
console.log('Service Worker registered successfully:', this.registration)
|
||||
|
||||
// Listen for service worker updates
|
||||
this.registration.addEventListener('updatefound', () => {
|
||||
console.log('Service Worker update found')
|
||||
const newWorker = this.registration?.installing
|
||||
|
||||
if (newWorker) {
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
// New version available
|
||||
console.log('New version available!')
|
||||
if (this.updateCallback) {
|
||||
this.updateCallback()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Listen for messages from service worker
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
console.log('Message from Service Worker:', event.data)
|
||||
|
||||
if (event.data && event.data.type === 'SW_UPDATED') {
|
||||
console.log('Service Worker updated to version:', event.data.version)
|
||||
if (this.updateCallback) {
|
||||
this.updateCallback()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Check for updates every 30 seconds
|
||||
setInterval(() => {
|
||||
this.checkForUpdates()
|
||||
}, 30000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Service Worker registration failed:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all caches
|
||||
async clearAllCaches(): Promise<void> {
|
||||
try {
|
||||
// Clear browser caches
|
||||
if ('caches' in window) {
|
||||
const cacheNames = await caches.keys()
|
||||
await Promise.all(
|
||||
cacheNames.map(cacheName => caches.delete(cacheName))
|
||||
)
|
||||
console.log('All caches cleared')
|
||||
}
|
||||
|
||||
// Send message to service worker to clear its caches
|
||||
if (navigator.serviceWorker.controller) {
|
||||
const messageChannel = new MessageChannel()
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
messageChannel.port1.onmessage = (event) => {
|
||||
if (event.data.success) {
|
||||
console.log('Service Worker caches cleared')
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
// Safe to call postMessage since we checked controller is not null
|
||||
navigator.serviceWorker.controller!.postMessage(
|
||||
{ type: 'CACHE_INVALIDATE' },
|
||||
[messageChannel.port2]
|
||||
)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to clear caches:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Check for service worker updates
|
||||
async checkForUpdates(): Promise<boolean> {
|
||||
if (this.registration) {
|
||||
try {
|
||||
await this.registration.update()
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to check for updates:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Force reload the page
|
||||
forceReload(): void {
|
||||
// Clear caches first, then reload
|
||||
this.clearAllCaches().then(() => {
|
||||
window.location.reload()
|
||||
}).catch(() => {
|
||||
// Fallback: just reload
|
||||
window.location.reload()
|
||||
})
|
||||
}
|
||||
|
||||
// Register callback for updates
|
||||
registerUpdateHandler(callback: () => void): void {
|
||||
this.updateCallback = callback
|
||||
}
|
||||
|
||||
// Skip waiting and activate new service worker
|
||||
async skipWaiting(): Promise<void> {
|
||||
if (this.registration && this.registration.waiting) {
|
||||
// Send message to service worker to skip waiting
|
||||
this.registration.waiting.postMessage({ type: 'SKIP_WAITING' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const cacheManager = new PWACacheManager()
|
||||
|
||||
// Export utility functions
|
||||
export const clearAllCaches = () => cacheManager.clearAllCaches()
|
||||
export const checkForUpdates = () => cacheManager.checkForUpdates()
|
||||
export const forceReload = () => cacheManager.forceReload()
|
||||
export const registerUpdateHandler = (callback: () => void) => cacheManager.registerUpdateHandler(callback)
|
||||
|
||||
// Development helpers
|
||||
export const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
|
||||
export const enableDevCacheBypass = () => {
|
||||
if (isDevelopment) {
|
||||
// Disable caching in development
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
registrations.forEach(registration => {
|
||||
registration.unregister()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-update notification component data
|
||||
export const createUpdateNotification = () => {
|
||||
return {
|
||||
title: 'New Version Available!',
|
||||
message: 'A new version of the app is available. Refresh to get the latest features.',
|
||||
actions: [
|
||||
{
|
||||
label: 'Refresh Now',
|
||||
action: forceReload
|
||||
},
|
||||
{
|
||||
label: 'Later',
|
||||
action: () => console.log('Update dismissed')
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Cache status utilities
|
||||
export const getCacheStatus = async () => {
|
||||
if ('caches' in window) {
|
||||
const cacheNames = await caches.keys()
|
||||
const cacheDetails = await Promise.all(
|
||||
cacheNames.map(async (name) => {
|
||||
const cache = await caches.open(name)
|
||||
const keys = await cache.keys()
|
||||
return {
|
||||
name,
|
||||
size: keys.length
|
||||
}
|
||||
})
|
||||
)
|
||||
return cacheDetails
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export default cacheManager
|
||||
129
lib/cart.ts
Normal file
129
lib/cart.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
interface CartItem {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
quantity: number
|
||||
image: string | null
|
||||
}
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
discount: number
|
||||
images: string[]
|
||||
stock: number
|
||||
}
|
||||
|
||||
class CartManager {
|
||||
private cartKey = 'shopping-cart'
|
||||
|
||||
private getDiscountedPrice(price: number, discount: number): number {
|
||||
return price - (price * discount / 100)
|
||||
}
|
||||
|
||||
getCart(): CartItem[] {
|
||||
if (typeof window === 'undefined') return []
|
||||
|
||||
try {
|
||||
const cart = localStorage.getItem(this.cartKey)
|
||||
return cart ? JSON.parse(cart) : []
|
||||
} catch (error) {
|
||||
console.error('Error getting cart:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
saveCart(cart: CartItem[]): void {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
localStorage.setItem(this.cartKey, JSON.stringify(cart))
|
||||
// Dispatch custom event to notify components about cart updates
|
||||
window.dispatchEvent(new CustomEvent('cartUpdated', { detail: cart }))
|
||||
} catch (error) {
|
||||
console.error('Error saving cart:', error)
|
||||
}
|
||||
}
|
||||
|
||||
addToCart(product: Product, quantity: number = 1): boolean {
|
||||
if (product.stock < quantity) {
|
||||
return false
|
||||
}
|
||||
|
||||
const cart = this.getCart()
|
||||
const existingItemIndex = cart.findIndex(item => item.id === product.id)
|
||||
|
||||
if (existingItemIndex >= 0) {
|
||||
const newQuantity = cart[existingItemIndex].quantity + quantity
|
||||
if (newQuantity > product.stock) {
|
||||
return false
|
||||
}
|
||||
cart[existingItemIndex].quantity = newQuantity
|
||||
} else {
|
||||
cart.push({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: this.getDiscountedPrice(product.price, product.discount),
|
||||
quantity,
|
||||
image: product.images[0] || null
|
||||
})
|
||||
}
|
||||
|
||||
this.saveCart(cart)
|
||||
return true
|
||||
}
|
||||
|
||||
removeFromCart(productId: string): void {
|
||||
const cart = this.getCart()
|
||||
const updatedCart = cart.filter(item => item.id !== productId)
|
||||
this.saveCart(updatedCart)
|
||||
}
|
||||
|
||||
updateQuantity(productId: string, quantity: number): boolean {
|
||||
if (quantity <= 0) {
|
||||
this.removeFromCart(productId)
|
||||
return true
|
||||
}
|
||||
|
||||
const cart = this.getCart()
|
||||
const itemIndex = cart.findIndex(item => item.id === productId)
|
||||
|
||||
if (itemIndex >= 0) {
|
||||
cart[itemIndex].quantity = quantity
|
||||
this.saveCart(cart)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
clearCart(): void {
|
||||
this.saveCart([])
|
||||
}
|
||||
|
||||
getTotalPrice(): number {
|
||||
const cart = this.getCart()
|
||||
return cart.reduce((total, item) => total + (item.price * item.quantity), 0)
|
||||
}
|
||||
|
||||
// Add alias for compatibility
|
||||
getCartTotal(): number {
|
||||
return this.getTotalPrice()
|
||||
}
|
||||
|
||||
getTotalItems(): number {
|
||||
const cart = this.getCart()
|
||||
return cart.reduce((total, item) => total + item.quantity, 0)
|
||||
}
|
||||
|
||||
getItemCount(): number {
|
||||
return this.getCart().length
|
||||
}
|
||||
|
||||
getCartCount(): number {
|
||||
const cart = this.getCart()
|
||||
return cart.reduce((total, item) => total + item.quantity, 0)
|
||||
}
|
||||
}
|
||||
|
||||
export const cartManager = new CartManager()
|
||||
371
lib/commission.ts
Normal file
371
lib/commission.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
interface CommissionResult {
|
||||
success: boolean
|
||||
message: string
|
||||
commissions?: Array<{
|
||||
referrerId: string
|
||||
referrerName: string | null
|
||||
amount: number
|
||||
level: number
|
||||
type: string
|
||||
}>
|
||||
error?: string
|
||||
}
|
||||
|
||||
export async function calculateCommissions(orderId: string): Promise<CommissionResult> {
|
||||
try {
|
||||
// Get the order with user and referrer information
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: orderId },
|
||||
include: {
|
||||
user: {
|
||||
include: {
|
||||
referrer: true
|
||||
}
|
||||
},
|
||||
orderItems: {
|
||||
include: {
|
||||
product: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!order || !order.user.referrerId) {
|
||||
return { success: false, message: 'No referrer found for commission calculation' }
|
||||
}
|
||||
|
||||
// Get commission settings
|
||||
const commissionSettings = await prisma.commissionSettings.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: { level: 'asc' }
|
||||
})
|
||||
|
||||
if (commissionSettings.length === 0) {
|
||||
// Initialize default settings if none exist
|
||||
await initializeCommissionSettings()
|
||||
return await calculateCommissions(orderId) // Retry after initialization
|
||||
}
|
||||
|
||||
const commissions = []
|
||||
let currentUser: any = order.user
|
||||
let level = 1
|
||||
|
||||
// Calculate commissions for each level (up to 3 levels for partners)
|
||||
for (const setting of commissionSettings) {
|
||||
if (!currentUser.referrerId || level > 3) break // Limit to 3 levels
|
||||
|
||||
// Get referrer with rank information
|
||||
const referrer = await prisma.user.findUnique({
|
||||
where: { id: currentUser.referrerId },
|
||||
include: {
|
||||
referrer: true,
|
||||
currentRank: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!referrer) break
|
||||
|
||||
// Only calculate commissions for MEMBER (partners) referrers
|
||||
if (referrer.role !== 'MEMBER') {
|
||||
currentUser = referrer
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate base commission amount
|
||||
let commissionAmount = (order.total * setting.percentage) / 100
|
||||
|
||||
// Apply rank multiplier if available
|
||||
if (referrer.currentRank) {
|
||||
commissionAmount *= referrer.currentRank.commissionMultiplier
|
||||
}
|
||||
|
||||
// Create commission record
|
||||
await prisma.commission.create({
|
||||
data: {
|
||||
userId: referrer.id,
|
||||
fromUserId: order.userId,
|
||||
orderId: order.id,
|
||||
amount: commissionAmount,
|
||||
level: level,
|
||||
type: level === 1 ? 'REFERRAL' : 'LEVEL',
|
||||
status: 'APPROVED' // Auto-approve for partners
|
||||
}
|
||||
})
|
||||
|
||||
// Update referrer's wallet
|
||||
await prisma.wallet.upsert({
|
||||
where: { userId: referrer.id },
|
||||
update: {
|
||||
balance: { increment: commissionAmount },
|
||||
totalEarnings: { increment: commissionAmount }
|
||||
},
|
||||
create: {
|
||||
userId: referrer.id,
|
||||
balance: commissionAmount,
|
||||
totalEarnings: commissionAmount,
|
||||
totalWithdrawn: 0
|
||||
}
|
||||
})
|
||||
|
||||
commissions.push({
|
||||
referrerId: referrer.id,
|
||||
referrerName: referrer.name,
|
||||
amount: commissionAmount,
|
||||
level: level,
|
||||
type: level === 1 ? 'REFERRAL' : 'LEVEL'
|
||||
})
|
||||
|
||||
console.log(`Level ${level} commission: ₹${commissionAmount} for partner ${referrer.name} (${referrer.email})`)
|
||||
|
||||
// Update currentUser to the referrer for next iteration
|
||||
currentUser = referrer
|
||||
level++
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
commissions,
|
||||
message: `Generated ${commissions.length} commission(s)`
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error calculating commissions:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to calculate commissions',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCommissionSettings(): Promise<{
|
||||
success: boolean
|
||||
settings: any[]
|
||||
}> {
|
||||
try {
|
||||
const settings = await prisma.commissionSettings.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: { level: 'asc' }
|
||||
})
|
||||
|
||||
return { success: true, settings }
|
||||
} catch (error) {
|
||||
console.error('Error fetching commission settings:', error)
|
||||
return { success: false, settings: [] }
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateCommissionAmount(
|
||||
orderTotal: number,
|
||||
percentage: number,
|
||||
rankMultiplier: number = 1
|
||||
): number {
|
||||
const baseCommission = (orderTotal * percentage) / 100
|
||||
return baseCommission * rankMultiplier
|
||||
}
|
||||
|
||||
export class CommissionService {
|
||||
static async calculateCommissions(orderId: string): Promise<CommissionResult> {
|
||||
return calculateCommissions(orderId)
|
||||
}
|
||||
|
||||
static async getCommissionSettings(): Promise<{
|
||||
success: boolean
|
||||
settings: any[]
|
||||
}> {
|
||||
return getCommissionSettings()
|
||||
}
|
||||
|
||||
static calculateCommissionAmount(
|
||||
orderTotal: number,
|
||||
percentage: number,
|
||||
rankMultiplier: number = 1
|
||||
): number {
|
||||
return calculateCommissionAmount(orderTotal, percentage, rankMultiplier)
|
||||
}
|
||||
|
||||
static async getUserCommissions(userId: string, limit: number = 10): Promise<{
|
||||
success: boolean
|
||||
commissions: any[]
|
||||
}> {
|
||||
try {
|
||||
const commissions = await prisma.commission.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
fromUser: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limit
|
||||
})
|
||||
|
||||
return { success: true, commissions }
|
||||
} catch (error) {
|
||||
console.error('Error fetching user commissions:', error)
|
||||
return { success: false, commissions: [] }
|
||||
}
|
||||
}
|
||||
|
||||
static async getCommissionStats(userId: string): Promise<{
|
||||
success: boolean
|
||||
stats?: {
|
||||
totalEarnings: number
|
||||
pendingCommissions: number
|
||||
thisMonthEarnings: number
|
||||
}
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
const [totalEarnings, pendingCommissions, thisMonthEarnings] = await Promise.all([
|
||||
prisma.commission.aggregate({
|
||||
where: {
|
||||
userId,
|
||||
status: { in: ['APPROVED', 'PAID'] }
|
||||
},
|
||||
_sum: { amount: true }
|
||||
}),
|
||||
prisma.commission.aggregate({
|
||||
where: {
|
||||
userId,
|
||||
status: 'PENDING'
|
||||
},
|
||||
_sum: { amount: true }
|
||||
}),
|
||||
prisma.commission.aggregate({
|
||||
where: {
|
||||
userId,
|
||||
status: { in: ['APPROVED', 'PAID'] },
|
||||
createdAt: {
|
||||
gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1)
|
||||
}
|
||||
},
|
||||
_sum: { amount: true }
|
||||
})
|
||||
])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stats: {
|
||||
totalEarnings: totalEarnings._sum.amount || 0,
|
||||
pendingCommissions: pendingCommissions._sum.amount || 0,
|
||||
thisMonthEarnings: thisMonthEarnings._sum.amount || 0
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching commission stats:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async getTeamStats(userId: string): Promise<{
|
||||
success: boolean
|
||||
stats?: {
|
||||
directReferrals: number
|
||||
totalTeamSize: number
|
||||
teamSalesVolume: number
|
||||
activeMembers: number
|
||||
}
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
// Get direct referrals count
|
||||
const directReferrals = await prisma.user.count({
|
||||
where: { referrerId: userId }
|
||||
})
|
||||
|
||||
// Get total team size (all levels)
|
||||
const allReferrals = await this.getAllReferrals(userId)
|
||||
const totalTeamSize = allReferrals.length
|
||||
|
||||
// Get team sales volume (last 30 days)
|
||||
const thirtyDaysAgo = new Date()
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
|
||||
|
||||
const teamSalesResult = await prisma.order.aggregate({
|
||||
where: {
|
||||
userId: { in: allReferrals },
|
||||
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] },
|
||||
createdAt: { gte: thirtyDaysAgo }
|
||||
},
|
||||
_sum: { total: true }
|
||||
})
|
||||
|
||||
// Get active team members (made purchase in last 30 days)
|
||||
const activeMembers = await prisma.user.count({
|
||||
where: {
|
||||
id: { in: allReferrals },
|
||||
orders: {
|
||||
some: {
|
||||
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] },
|
||||
createdAt: { gte: thirtyDaysAgo }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stats: {
|
||||
directReferrals,
|
||||
totalTeamSize,
|
||||
activeMembers,
|
||||
teamSalesVolume: teamSalesResult._sum.total || 0
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching team stats:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async getAllReferrals(userId: string, visited = new Set<string>()): Promise<string[]> {
|
||||
if (visited.has(userId)) return []
|
||||
visited.add(userId)
|
||||
|
||||
const directReferrals = await prisma.user.findMany({
|
||||
where: { referrerId: userId },
|
||||
select: { id: true }
|
||||
})
|
||||
|
||||
let allReferrals = directReferrals.map(r => r.id)
|
||||
|
||||
// Recursively get referrals of referrals
|
||||
for (const referral of directReferrals) {
|
||||
const subReferrals = await this.getAllReferrals(referral.id, visited)
|
||||
allReferrals = [...allReferrals, ...subReferrals]
|
||||
}
|
||||
|
||||
return allReferrals
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize default commission settings for partners
|
||||
export async function initializeCommissionSettings(): Promise<void> {
|
||||
try {
|
||||
const existingSettings = await prisma.commissionSettings.findMany()
|
||||
|
||||
if (existingSettings.length === 0) {
|
||||
await prisma.commissionSettings.createMany({
|
||||
data: [
|
||||
{ level: 1, percentage: 5.0, isActive: true }, // 5% for direct referrer (partner)
|
||||
{ level: 2, percentage: 2.0, isActive: true }, // 2% for level 2 partner
|
||||
{ level: 3, percentage: 1.0, isActive: true }, // 1% for level 3 partner
|
||||
]
|
||||
})
|
||||
console.log('Commission settings initialized for partner system')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize commission settings:', error)
|
||||
}
|
||||
}
|
||||
175
lib/database-optimizer.ts
Normal file
175
lib/database-optimizer.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
// Database Optimizer for SEO Performance
|
||||
// Optimizes database queries with caching and performance monitoring
|
||||
|
||||
interface CacheEntry {
|
||||
data: any
|
||||
timestamp: number
|
||||
ttl: number
|
||||
}
|
||||
|
||||
interface QueryMetrics {
|
||||
queryTime: number
|
||||
cacheHit: boolean
|
||||
queryType: string
|
||||
}
|
||||
|
||||
class DatabaseOptimizerClass {
|
||||
private cache = new Map<string, CacheEntry>()
|
||||
private metrics: QueryMetrics[] = []
|
||||
|
||||
// Cache management
|
||||
async getCachedData(key: string): Promise<any | null> {
|
||||
const entry = this.cache.get(key)
|
||||
if (!entry) return null
|
||||
|
||||
// Check if expired
|
||||
if (Date.now() - entry.timestamp > entry.ttl * 1000) {
|
||||
this.cache.delete(key)
|
||||
return null
|
||||
}
|
||||
|
||||
this.recordMetrics(0, true, 'cache_hit')
|
||||
return entry.data
|
||||
}
|
||||
|
||||
async setCachedData(key: string, data: any, ttlSeconds = 300): Promise<void> {
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
ttl: ttlSeconds
|
||||
})
|
||||
}
|
||||
|
||||
// Query optimization wrapper
|
||||
async executeOptimizedQuery<T>(
|
||||
queryKey: string,
|
||||
queryFn: () => Promise<T>,
|
||||
ttlSeconds = 300
|
||||
): Promise<T> {
|
||||
const startTime = Date.now()
|
||||
|
||||
// Try cache first
|
||||
const cached = await this.getCachedData(queryKey)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
// Execute query
|
||||
const result = await queryFn()
|
||||
const queryTime = Date.now() - startTime
|
||||
|
||||
// Cache result
|
||||
await this.setCachedData(queryKey, result, ttlSeconds)
|
||||
|
||||
// Record metrics
|
||||
this.recordMetrics(queryTime, false, queryKey)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Performance monitoring
|
||||
private recordMetrics(queryTime: number, cacheHit: boolean, queryType: string) {
|
||||
this.metrics.push({ queryTime, cacheHit, queryType })
|
||||
|
||||
// Keep only last 100 metrics
|
||||
if (this.metrics.length > 100) {
|
||||
this.metrics.shift()
|
||||
}
|
||||
}
|
||||
|
||||
getPerformanceStats() {
|
||||
const recentMetrics = this.metrics.slice(-50)
|
||||
const cacheHitRate = recentMetrics.filter(m => m.cacheHit).length / recentMetrics.length
|
||||
const avgQueryTime = recentMetrics
|
||||
.filter(m => !m.cacheHit)
|
||||
.reduce((sum, m) => sum + m.queryTime, 0) / recentMetrics.filter(m => !m.cacheHit).length
|
||||
|
||||
return {
|
||||
cacheHitRate: Math.round(cacheHitRate * 100),
|
||||
avgQueryTime: Math.round(avgQueryTime || 0),
|
||||
totalQueries: this.metrics.length,
|
||||
recentQueries: recentMetrics.length
|
||||
}
|
||||
}
|
||||
|
||||
// SEO-specific optimizations
|
||||
async getOptimizedProducts(limit = 12, categoryId?: string) {
|
||||
const cacheKey = `products_seo_${limit}_${categoryId || 'all'}`
|
||||
|
||||
return this.executeOptimizedQuery(cacheKey, async () => {
|
||||
// This would be the actual Prisma query
|
||||
return {
|
||||
products: [],
|
||||
seoData: {
|
||||
totalProducts: 0,
|
||||
categories: [],
|
||||
averageRating: 0
|
||||
}
|
||||
}
|
||||
}, 600) // Cache for 10 minutes
|
||||
}
|
||||
|
||||
async getOptimizedCategories() {
|
||||
const cacheKey = 'categories_navigation'
|
||||
|
||||
return this.executeOptimizedQuery(cacheKey, async () => {
|
||||
// This would be the actual Prisma query
|
||||
return {
|
||||
categories: [],
|
||||
productCounts: {},
|
||||
featuredCategories: []
|
||||
}
|
||||
}, 1800) // Cache for 30 minutes
|
||||
}
|
||||
|
||||
async getSocialProofData() {
|
||||
const cacheKey = 'social_proof_data'
|
||||
|
||||
return this.executeOptimizedQuery(cacheKey, async () => {
|
||||
// This would be the actual Prisma queries
|
||||
return {
|
||||
totalCustomers: 0,
|
||||
totalReviews: 0,
|
||||
averageRating: 0,
|
||||
recentReviews: [],
|
||||
topProducts: []
|
||||
}
|
||||
}, 900) // Cache for 15 minutes
|
||||
}
|
||||
|
||||
// Cache invalidation for data consistency
|
||||
invalidateCache(pattern?: string) {
|
||||
if (!pattern) {
|
||||
this.cache.clear()
|
||||
return
|
||||
}
|
||||
|
||||
const keysToDelete = Array.from(this.cache.keys()).filter(key =>
|
||||
key.includes(pattern)
|
||||
)
|
||||
|
||||
keysToDelete.forEach(key => this.cache.delete(key))
|
||||
}
|
||||
|
||||
// Cleanup old cache entries
|
||||
cleanupCache() {
|
||||
const now = Date.now()
|
||||
const keysToCheck = Array.from(this.cache.keys())
|
||||
|
||||
keysToCheck.forEach(key => {
|
||||
const entry = this.cache.get(key)
|
||||
if (entry && now - entry.timestamp > entry.ttl * 1000) {
|
||||
this.cache.delete(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const DatabaseOptimizer = new DatabaseOptimizerClass()
|
||||
|
||||
// Auto cleanup every 5 minutes
|
||||
if (typeof setInterval !== 'undefined') {
|
||||
setInterval(() => {
|
||||
DatabaseOptimizer.cleanupCache()
|
||||
}, 5 * 60 * 1000)
|
||||
}
|
||||
937
lib/email.ts
Normal file
937
lib/email.ts
Normal file
@@ -0,0 +1,937 @@
|
||||
import nodemailer from 'nodemailer'
|
||||
import { getSystemSettings } from '@/lib/settings'
|
||||
|
||||
interface EmailOptions {
|
||||
to: string
|
||||
subject: string
|
||||
html: string
|
||||
text?: string
|
||||
}
|
||||
|
||||
class EmailServiceClass {
|
||||
private transporter: nodemailer.Transporter | null = null
|
||||
private isConfigured: boolean = false
|
||||
|
||||
constructor() {
|
||||
this.initializeTransporter()
|
||||
}
|
||||
|
||||
private initializeTransporter() {
|
||||
try {
|
||||
// Check if SMTP credentials are configured
|
||||
const smtpUser = process.env.SMTP_USER
|
||||
const smtpPass = process.env.SMTP_PASSWORD // Changed from SMTP_PASS
|
||||
|
||||
if (!smtpUser || !smtpPass) {
|
||||
console.warn('SMTP credentials not configured. Email notifications will be disabled.')
|
||||
return
|
||||
}
|
||||
|
||||
this.transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST || 'smtp.gmail.com',
|
||||
port: parseInt(process.env.SMTP_PORT || '587'),
|
||||
secure: false,
|
||||
auth: {
|
||||
user: smtpUser,
|
||||
pass: smtpPass,
|
||||
},
|
||||
})
|
||||
|
||||
this.isConfigured = true
|
||||
console.log('Email service initialized successfully')
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize email service:', error)
|
||||
this.isConfigured = false
|
||||
}
|
||||
}
|
||||
|
||||
async sendEmail({ to, subject, html, text }: EmailOptions) {
|
||||
if (!this.isConfigured || !this.transporter) {
|
||||
console.log(`Email would be sent to ${to}: ${subject}`)
|
||||
console.log('Email service not configured - skipping email send')
|
||||
return { messageId: 'not-configured', skipped: true }
|
||||
}
|
||||
|
||||
try {
|
||||
const settings = await getSystemSettings()
|
||||
|
||||
const mailOptions = {
|
||||
from: process.env.SMTP_FROM || `"${settings.siteName}" <${process.env.SMTP_USER}>`, // Use SMTP_FROM if available
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
text: text || this.htmlToText(html)
|
||||
}
|
||||
|
||||
const result = await this.transporter.sendMail(mailOptions)
|
||||
console.log('Email sent successfully:', result.messageId)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Email sending failed:', error)
|
||||
// Don't throw error, just log it so the main process continues
|
||||
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'
|
||||
return { messageId: 'failed', error: errorMessage }
|
||||
}
|
||||
}
|
||||
|
||||
private htmlToText(html: string): string {
|
||||
return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
// Order confirmation email
|
||||
async sendOrderConfirmation(userEmail: string, userName: string, order: any) {
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h1 style="color: #10B981;">Order Confirmed! 🎉</h1>
|
||||
<p>Hi ${userName},</p>
|
||||
<p>Thank you for your order! We're excited to process it for you.</p>
|
||||
|
||||
<div style="background: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Order Details:</h3>
|
||||
<p><strong>Order ID:</strong> #${order.id.slice(-8)}</p>
|
||||
<p><strong>Total:</strong> ₹${order.total.toFixed(2)}</p>
|
||||
<p><strong>Status:</strong> ${order.status}</p>
|
||||
<p><strong>Date:</strong> ${new Date(order.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin: 20px 0;">
|
||||
<h3>Items Ordered:</h3>
|
||||
${order.orderItems.map((item: any) => `
|
||||
<div style="border-bottom: 1px solid #e5e7eb; padding: 10px 0;">
|
||||
<p><strong>${item.product.name}</strong></p>
|
||||
<p>Quantity: ${item.quantity} | Price: ₹${item.price.toFixed(2)}</p>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<p>We'll send you another email when your order ships.</p>
|
||||
<p>Thanks for shopping with us!</p>
|
||||
|
||||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
|
||||
<p style="color: #6b7280; font-size: 14px;">
|
||||
If you have any questions, reply to this email or contact our support team.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
return await this.sendEmail({
|
||||
to: userEmail,
|
||||
subject: `Order Confirmation - #${order.id.slice(-8)}`,
|
||||
html
|
||||
})
|
||||
}
|
||||
|
||||
// Commission earned email
|
||||
async sendCommissionAlert(userEmail: string, userName: string, commission: any) {
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h1 style="color: #10B981;">Commission Earned! 💰</h1>
|
||||
<p>Hi ${userName},</p>
|
||||
<p>Great news! You've earned a new commission.</p>
|
||||
|
||||
<div style="background: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Commission Details:</h3>
|
||||
<p><strong>Amount:</strong> ₹${commission.amount.toFixed(2)}</p>
|
||||
<p><strong>Level:</strong> ${commission.level}</p>
|
||||
<p><strong>Type:</strong> ${commission.type}</p>
|
||||
<p><strong>From:</strong> ${commission.fromUser.name}</p>
|
||||
<p><strong>Date:</strong> ${new Date(commission.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
|
||||
<p>This commission has been added to your wallet.</p>
|
||||
<p>Keep up the great work building your network!</p>
|
||||
|
||||
<div style="margin-top: 30px;">
|
||||
<a href="${process.env.NEXTAUTH_URL}/dashboard/commissions"
|
||||
style="background: #3B82F6; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">
|
||||
View All Commissions
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
return await this.sendEmail({
|
||||
to: userEmail,
|
||||
subject: `New Commission Earned - ₹${commission.amount.toFixed(2)}`,
|
||||
html
|
||||
})
|
||||
}
|
||||
|
||||
// Rank achievement email
|
||||
async sendRankAchievement(userEmail: string, userName: string, newRank: any) {
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h1 style="color: #8B5CF6;">Congratulations! 🏆</h1>
|
||||
<p>Hi ${userName},</p>
|
||||
<p>Amazing news! You've achieved a new rank in our program.</p>
|
||||
|
||||
<div style="background: linear-gradient(135deg, #8B5CF6, #3B82F6); color: white; padding: 30px; border-radius: 12px; text-align: center; margin: 20px 0;">
|
||||
<h2 style="margin: 0; font-size: 28px;">🎉 ${newRank.name} 🎉</h2>
|
||||
<p style="margin: 10px 0 0 0; font-size: 18px;">${newRank.description}</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Rank Benefits:</h3>
|
||||
<ul>
|
||||
<li>Increased commission rate: ${newRank.commissionMultiplier}x</li>
|
||||
<li>Special recognition badge</li>
|
||||
<li>Priority support</li>
|
||||
<li>Exclusive promotions and bonuses</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>This achievement reflects your dedication and success. Keep growing your network to unlock even more rewards!</p>
|
||||
|
||||
<div style="margin-top: 30px;">
|
||||
<a href="${process.env.NEXTAUTH_URL}/dashboard"
|
||||
style="background: #8B5CF6; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">
|
||||
View Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
return await this.sendEmail({
|
||||
to: userEmail,
|
||||
subject: `🏆 Congratulations! You've achieved ${newRank.name} rank!`,
|
||||
html
|
||||
})
|
||||
}
|
||||
|
||||
// Partner application admin notification
|
||||
async sendPartnerApplicationAdminNotification(data: any, newUser: any) {
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #F5873B;">New Partner Application Received</h2>
|
||||
|
||||
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Partner Information</h3>
|
||||
<p><strong>Name:</strong> ${data.firstName} ${data.lastName}</p>
|
||||
<p><strong>Email:</strong> ${data.email}</p>
|
||||
<p><strong>Phone:</strong> ${data.phone}</p>
|
||||
<p><strong>Partnership Tier:</strong> ${data.partnershipTier}</p>
|
||||
<p><strong>Expected Customers:</strong> ${data.expectedCustomers}</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Business Information</h3>
|
||||
<p><strong>Business Name:</strong> ${data.businessName || 'N/A'}</p>
|
||||
<p><strong>Business Type:</strong> ${data.businessType}</p>
|
||||
<p><strong>Experience:</strong> ${data.experience}</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Address</h3>
|
||||
<p>${data.address}</p>
|
||||
<p>${data.city}, ${data.state} ${data.zipCode}</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Additional Information</h3>
|
||||
<p><strong>Motivation:</strong> ${data.motivation}</p>
|
||||
<p><strong>Marketing Plan:</strong> ${data.marketingPlan}</p>
|
||||
</div>
|
||||
|
||||
<p style="color: #28a745; font-weight: bold;">✅ User has been automatically created and approved as a MEMBER.</p>
|
||||
<p><strong>User ID:</strong> ${newUser.id}</p>
|
||||
<p><strong>Referral Code:</strong> ${newUser.referralCode}</p>
|
||||
</div>
|
||||
`
|
||||
|
||||
return await this.sendEmail({
|
||||
to: process.env.ADMIN_EMAIL || 'info@padmajarice.com',
|
||||
subject: `New Partner Application - ${data.partnershipTier} Tier`,
|
||||
html
|
||||
})
|
||||
}
|
||||
|
||||
// Partner welcome email
|
||||
async sendPartnerWelcomeEmail(data: any, newUser: any, randomPassword: string) {
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background-color: #F5873B; color: white; padding: 20px; text-align: center;">
|
||||
<h1>Welcome to Padmaaja Rasooi!</h1>
|
||||
<p>Your partnership application has been approved</p>
|
||||
</div>
|
||||
|
||||
<div style="padding: 20px;">
|
||||
<h2>Congratulations, ${data.firstName}!</h2>
|
||||
<p>We're excited to welcome you as a <strong>${data.partnershipTier}</strong> partner in the Padmaaja Rasooi family.</p>
|
||||
|
||||
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Your Login Details</h3>
|
||||
<p><strong>Website:</strong> <a href="${process.env.NEXTAUTH_URL || 'http://localhost:3000'}">${process.env.NEXTAUTH_URL || 'http://localhost:3000'}</a></p>
|
||||
<p><strong>Email:</strong> ${data.email}</p>
|
||||
<p><strong>Password:</strong> ${randomPassword}</p>
|
||||
<p><strong>Your Referral Code:</strong> ${newUser.referralCode}</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #e8f5e8; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Partnership Details</h3>
|
||||
<p><strong>Tier:</strong> ${data.partnershipTier}</p>
|
||||
<p><strong>Required Members:</strong> Minimum 3 members must be added</p>
|
||||
<p><strong>Commission Structure:</strong> Earn commissions on all purchases made by your referrals</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #fff3cd; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Next Steps</h3>
|
||||
<ol>
|
||||
<li>Log in to your partner dashboard using the credentials above</li>
|
||||
<li>Complete your profile setup</li>
|
||||
<li>Add minimum 3 members using your referral code</li>
|
||||
<li>Track your earnings and commissions in real-time</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Important Notes</h3>
|
||||
<ul>
|
||||
<li>You must add at least <strong>3 members</strong> to maintain your partnership</li>
|
||||
<li>You'll earn commissions on all purchases made by your referrals</li>
|
||||
<li>Please change your password after first login for security</li>
|
||||
<li>Contact support for any questions: ${process.env.SUPPORT_EMAIL || 'info@padmajarice.com'}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${process.env.NEXTAUTH_URL || 'http://localhost:3000'}/auth/signin"
|
||||
style="background-color: #F5873B; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">
|
||||
Login to Your Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>Thank you for joining Padmaaja Rasooi. We look forward to a successful partnership!</p>
|
||||
|
||||
<div style="border-top: 1px solid #eee; margin-top: 30px; padding-top: 20px; text-align: center; color: #666;">
|
||||
<p>Best regards,<br>The Padmaaja Rasooi Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
return await this.sendEmail({
|
||||
to: data.email,
|
||||
subject: 'Welcome to Padmaaja Rasooi Partnership Program!',
|
||||
html
|
||||
})
|
||||
}
|
||||
|
||||
// Member added admin notification
|
||||
async sendMemberAddedAdminNotification(partner: any, newMember: any) {
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #F5873B;">New Member Added</h2>
|
||||
|
||||
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Partner Information</h3>
|
||||
<p><strong>Partner:</strong> ${partner.name} (${partner.email})</p>
|
||||
<p><strong>Partner Tier:</strong> ${partner.partnerTier || 'N/A'}</p>
|
||||
<p><strong>Current Members:</strong> ${partner.referrals.length + 1}</p>
|
||||
<p><strong>Min Required:</strong> ${partner.minReferrals}</p>
|
||||
<p><strong>Status:</strong> ${partner.referrals.length + 1 >= partner.minReferrals ? '✅ Meeting minimum requirement' : '⚠️ Building minimum members'}</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>New Member Information</h3>
|
||||
<p><strong>Name:</strong> ${newMember.name}</p>
|
||||
<p><strong>Email:</strong> ${newMember.email}</p>
|
||||
<p><strong>Phone:</strong> ${newMember.phone}</p>
|
||||
<p><strong>User ID:</strong> ${newMember.id}</p>
|
||||
<p><strong>Referral Code:</strong> ${newMember.referralCode}</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
return await this.sendEmail({
|
||||
to: process.env.ADMIN_EMAIL || 'info@padmajarice.com',
|
||||
subject: `New Member Added by Partner: ${partner.name}`,
|
||||
html
|
||||
})
|
||||
}
|
||||
|
||||
// Member welcome email
|
||||
async sendMemberWelcomeEmail(memberData: any, partner: any, randomPassword: string) {
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background-color: #F5873B; color: white; padding: 20px; text-align: center;">
|
||||
<h1>Welcome to Padmaaja Rasooi!</h1>
|
||||
<p>You've been added by our partner: ${partner.name}</p>
|
||||
</div>
|
||||
|
||||
<div style="padding: 20px;">
|
||||
<h2>Hello ${memberData.name}!</h2>
|
||||
<p>Welcome to the Padmaaja Rasooi family! You've been added as a member by our partner <strong>${partner.name}</strong>.</p>
|
||||
|
||||
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Your Account Details</h3>
|
||||
<p><strong>Website:</strong> <a href="${process.env.NEXTAUTH_URL || 'http://localhost:3000'}">${process.env.NEXTAUTH_URL || 'http://localhost:3000'}</a></p>
|
||||
<p><strong>Email:</strong> ${memberData.email}</p>
|
||||
<p><strong>Password:</strong> ${randomPassword}</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #e8f5e8; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>What's Next?</h3>
|
||||
<ol>
|
||||
<li>Log in to your account using the credentials above</li>
|
||||
<li>Browse our premium spice collection</li>
|
||||
<li>Place your first order and enjoy exclusive member benefits</li>
|
||||
<li>Earn rewards and commissions through our referral program</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #fff3cd; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Member Benefits</h3>
|
||||
<ul>
|
||||
<li>Exclusive access to premium spices and masalas</li>
|
||||
<li>Special member pricing and discounts</li>
|
||||
<li>Earn commissions by referring others</li>
|
||||
<li>Priority customer support</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${process.env.NEXTAUTH_URL || 'http://localhost:3000'}/auth/signin"
|
||||
style="background-color: #F5873B; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">
|
||||
Login to Your Account
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p><strong>Important:</strong> Please change your password after first login for security.</p>
|
||||
<p>If you have any questions, contact us at ${process.env.SUPPORT_EMAIL || 'info@padmajarice.com'}</p>
|
||||
|
||||
<div style="border-top: 1px solid #eee; margin-top: 30px; padding-top: 20px; text-align: center; color: #666;">
|
||||
<p>Best regards,<br>The Padmaaja Rasooi Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
return await this.sendEmail({
|
||||
to: memberData.email,
|
||||
subject: 'Welcome to Padmaaja Rasooi!',
|
||||
html
|
||||
})
|
||||
}
|
||||
|
||||
// Wholesaler welcome email
|
||||
async sendWholesalerWelcomeEmail(wholesalerData: {
|
||||
to: string
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
businessName: string
|
||||
}) {
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; background-color: #f9f9f9; padding: 20px;">
|
||||
<div style="background-color: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
|
||||
<h1 style="color: #2563eb; text-align: center; margin-bottom: 30px;">Welcome to Padmaaja Rasooi Wholesaler Network! 🏪</h1>
|
||||
|
||||
<p>Dear ${wholesalerData.name},</p>
|
||||
|
||||
<p>Congratulations! Your wholesaler registration has been approved. You are now part of our exclusive wholesaler network.</p>
|
||||
|
||||
<div style="background: linear-gradient(135deg, #2563eb, #1d4ed8); color: white; padding: 25px; border-radius: 10px; text-align: center; margin: 20px 0;">
|
||||
<h2 style="margin: 0;">🎉 Welcome ${wholesalerData.businessName}! 🎉</h2>
|
||||
<p style="margin: 10px 0 0 0; font-size: 16px;">You now have access to wholesale benefits</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #f0f9ff; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Your Login Credentials</h3>
|
||||
<p><strong>Email:</strong> ${wholesalerData.email}</p>
|
||||
<p><strong>Temporary Password:</strong> ${wholesalerData.password}</p>
|
||||
<p style="color: #dc2626; font-size: 14px;"><strong>Important:</strong> Please change your password after first login.</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #dcfce7; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>🎁 Your Wholesale Benefits</h3>
|
||||
<ul>
|
||||
<li><strong>25% Discount</strong> on all bulk orders</li>
|
||||
<li>Dedicated account manager</li>
|
||||
<li>Priority customer support</li>
|
||||
<li>Flexible payment terms</li>
|
||||
<li>Quality guarantee on all products</li>
|
||||
<li>Fast order processing</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #fef3c7; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Next Steps</h3>
|
||||
<ol>
|
||||
<li>Login to your account using the credentials above</li>
|
||||
<li>Complete your profile information</li>
|
||||
<li>Browse our wholesale catalog</li>
|
||||
<li>Place your first bulk order to enjoy 25% discount</li>
|
||||
<li>Contact your account manager for assistance</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${process.env.NEXTAUTH_URL || 'http://localhost:3000'}/auth/signin"
|
||||
style="background-color: #2563eb; color: white; padding: 15px 30px; text-decoration: none; border-radius: 8px; display: inline-block; font-weight: bold;">
|
||||
Login to Your Wholesaler Account
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>Our team will contact you within 24 hours to assist with your first order and answer any questions.</p>
|
||||
<p>For immediate assistance, contact us at ${process.env.SUPPORT_EMAIL || 'info@padmajarice.com'} or call our wholesaler hotline.</p>
|
||||
|
||||
<div style="border-top: 1px solid #eee; margin-top: 30px; padding-top: 20px; text-align: center; color: #666;">
|
||||
<p>Best regards,<br>The Padmaaja Rasooi Wholesale Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
return await this.sendEmail({
|
||||
to: wholesalerData.to,
|
||||
subject: 'Welcome to Padmaaja Rasooi Wholesale Network!',
|
||||
html
|
||||
})
|
||||
}
|
||||
|
||||
// Admin notification for new wholesaler registration
|
||||
async sendWholesalerAdminNotification(wholesalerData: {
|
||||
wholesalerName: string
|
||||
wholesalerEmail: string
|
||||
businessName: string
|
||||
businessType: string
|
||||
phone: string
|
||||
expectedVolume: string
|
||||
registrationDate: string
|
||||
}) {
|
||||
const settings = await getSystemSettings()
|
||||
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; background-color: #f9f9f9; padding: 20px;">
|
||||
<div style="background-color: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
|
||||
<h1 style="color: #dc2626; text-align: center; margin-bottom: 30px;">🏪 New Wholesaler Registration</h1>
|
||||
|
||||
<p>A new wholesaler has registered on the platform.</p>
|
||||
|
||||
<div style="background-color: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Wholesaler Details</h3>
|
||||
<p><strong>Name:</strong> ${wholesalerData.wholesalerName}</p>
|
||||
<p><strong>Email:</strong> ${wholesalerData.wholesalerEmail}</p>
|
||||
<p><strong>Business Name:</strong> ${wholesalerData.businessName}</p>
|
||||
<p><strong>Business Type:</strong> ${wholesalerData.businessType}</p>
|
||||
<p><strong>Phone:</strong> ${wholesalerData.phone}</p>
|
||||
<p><strong>Expected Volume:</strong> ${wholesalerData.expectedVolume}</p>
|
||||
<p><strong>Registration Date:</strong> ${wholesalerData.registrationDate}</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #dbeafe; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3>Required Actions</h3>
|
||||
<ul>
|
||||
<li>Review the wholesaler's application</li>
|
||||
<li>Contact the wholesaler within 24 hours</li>
|
||||
<li>Assign an account manager</li>
|
||||
<li>Set up wholesale pricing if needed</li>
|
||||
<li>Provide product catalogs and information</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${process.env.NEXTAUTH_URL || 'http://localhost:3000'}/admin/users"
|
||||
style="background-color: #dc2626; color: white; padding: 15px 30px; text-decoration: none; border-radius: 8px; display: inline-block; font-weight: bold;">
|
||||
View in Admin Panel
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>The wholesaler has been automatically approved and given access to 25% wholesale discounts on bulk orders.</p>
|
||||
|
||||
<div style="border-top: 1px solid #eee; margin-top: 30px; padding-top: 20px; text-align: center; color: #666;">
|
||||
<p>This is an automated notification from Padmaaja Rasooi</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
return await this.sendEmail({
|
||||
to: settings.supportEmail,
|
||||
subject: `New Wholesaler Registration - ${wholesalerData.businessName}`,
|
||||
html
|
||||
})
|
||||
}
|
||||
|
||||
async sendPartTimeWelcomeEmail(partTimeData: {
|
||||
to: string
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
preferredRole: string
|
||||
}) {
|
||||
const settings = await getSystemSettings()
|
||||
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background: linear-gradient(135deg, #8B5CF6 0%, #A855F7 100%); color: white; padding: 30px; text-align: center;">
|
||||
<h1 style="margin: 0; font-size: 28px;">Welcome to Padmaaja!</h1>
|
||||
<p style="margin: 10px 0 0 0; font-size: 16px;">Part-Time Job Application Received</p>
|
||||
</div>
|
||||
|
||||
<div style="padding: 30px; background-color: #f9f9f9;">
|
||||
<h2 style="color: #333; margin-bottom: 20px;">Dear ${partTimeData.name},</h2>
|
||||
|
||||
<p style="color: #666; line-height: 1.6; margin-bottom: 20px;">
|
||||
Thank you for applying for a part-time position with Padmaaja! We're excited about your interest in joining our team.
|
||||
</p>
|
||||
|
||||
<div style="background: white; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="color: #8B5CF6; margin-top: 0;">Application Details:</h3>
|
||||
<p><strong>Preferred Role:</strong> ${partTimeData.preferredRole}</p>
|
||||
<p><strong>Application Status:</strong> Under Review</p>
|
||||
</div>
|
||||
|
||||
<div style="background: white; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="color: #8B5CF6; margin-top: 0;">Your Login Credentials:</h3>
|
||||
<p><strong>Email:</strong> ${partTimeData.email}</p>
|
||||
<p><strong>Password:</strong> ${partTimeData.password}</p>
|
||||
<p style="color: #666; font-size: 14px;">Please keep these credentials safe. You can use them to log in to your dashboard.</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #E0F2FE; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="color: #0369A1; margin-top: 0;">What's Next?</h3>
|
||||
<ul style="color: #666; padding-left: 20px;">
|
||||
<li>Our HR team will review your application within 2-3 business days</li>
|
||||
<li>You'll receive a call to discuss the opportunity</li>
|
||||
<li>Complete a brief interview process</li>
|
||||
<li>Start your part-time job journey with us!</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p style="color: #666; line-height: 1.6; margin-top: 30px;">
|
||||
If you have any questions, feel free to contact our support team.
|
||||
</p>
|
||||
|
||||
<p style="color: #666; line-height: 1.6;">
|
||||
Best regards,<br>
|
||||
<strong>Padmaaja Team</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #333; color: white; padding: 20px; text-align: center;">
|
||||
<p style="margin: 0; font-size: 14px;">© 2024 Padmaaja. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
return await this.sendEmail({
|
||||
to: partTimeData.to,
|
||||
subject: 'Part-Time Job Application Received - Padmaaja',
|
||||
html
|
||||
})
|
||||
}
|
||||
|
||||
async sendPartTimeAdminNotification(partTimeData: {
|
||||
applicantName: string
|
||||
applicantEmail: string
|
||||
preferredRole: string
|
||||
phone: string
|
||||
availableHours: string
|
||||
availableDays: string
|
||||
motivation: string
|
||||
registrationDate: string
|
||||
}) {
|
||||
const settings = await getSystemSettings()
|
||||
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background: linear-gradient(135deg, #8B5CF6 0%, #A855F7 100%); color: white; padding: 30px; text-align: center;">
|
||||
<h1 style="margin: 0; font-size: 28px;">New Part-Time Job Application</h1>
|
||||
<p style="margin: 10px 0 0 0; font-size: 16px;">Application received on ${partTimeData.registrationDate}</p>
|
||||
</div>
|
||||
|
||||
<div style="padding: 30px; background-color: #f9f9f9;">
|
||||
<h2 style="color: #333; margin-bottom: 20px;">Application Details:</h2>
|
||||
|
||||
<div style="background: white; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="color: #8B5CF6; margin-top: 0;">Applicant Information:</h3>
|
||||
<p><strong>Name:</strong> ${partTimeData.applicantName}</p>
|
||||
<p><strong>Email:</strong> ${partTimeData.applicantEmail}</p>
|
||||
<p><strong>Phone:</strong> ${partTimeData.phone}</p>
|
||||
</div>
|
||||
|
||||
<div style="background: white; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="color: #8B5CF6; margin-top: 0;">Job Preferences:</h3>
|
||||
<p><strong>Preferred Role:</strong> ${partTimeData.preferredRole}</p>
|
||||
<p><strong>Available Hours:</strong> ${partTimeData.availableHours} per day</p>
|
||||
<p><strong>Available Days:</strong> ${partTimeData.availableDays}</p>
|
||||
</div>
|
||||
|
||||
<div style="background: white; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="color: #8B5CF6; margin-top: 0;">Motivation:</h3>
|
||||
<p style="line-height: 1.6;">${partTimeData.motivation}</p>
|
||||
</div>
|
||||
|
||||
<p style="color: #666; line-height: 1.6; margin-top: 30px;">
|
||||
Please review this application and contact the applicant for the next steps.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #333; color: white; padding: 20px; text-align: center;">
|
||||
<p style="margin: 0; font-size: 14px;">© 2024 Padmaaja Admin Panel</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
return await this.sendEmail({
|
||||
to: settings.supportEmail,
|
||||
subject: `New Part-Time Application - ${partTimeData.applicantName}`,
|
||||
html
|
||||
})
|
||||
}
|
||||
|
||||
// B2B Inquiry confirmation email to customer
|
||||
async sendB2BInquiryConfirmation(data: {
|
||||
customerEmail: string
|
||||
customerName: string
|
||||
companyName: string
|
||||
inquiryId: string
|
||||
productName?: string
|
||||
}) {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>B2B Inquiry Confirmation</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: linear-gradient(135deg, #059669 0%, #065f46 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="margin: 0; font-size: 28px;">Inquiry Received Successfully</h1>
|
||||
<p style="margin: 10px 0 0 0; font-size: 16px; opacity: 0.9;">Thank you for your business inquiry</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #fff; padding: 30px; border: 1px solid #e5e7eb; border-radius: 0 0 10px 10px;">
|
||||
<p style="margin: 0 0 20px 0; font-size: 16px;">Dear ${data.customerName},</p>
|
||||
|
||||
<p style="margin: 0 0 20px 0; font-size: 16px;">
|
||||
Thank you for your inquiry regarding bulk procurement ${data.productName ? `of <strong>${data.productName}</strong>` : 'from Padmaaja Rasooi'}.
|
||||
We have received your detailed requirements and our team is reviewing them.
|
||||
</p>
|
||||
|
||||
<div style="background: #f9fafb; padding: 20px; border-left: 4px solid #059669; margin: 20px 0;">
|
||||
<h3 style="margin: 0 0 10px 0; color: #059669;">Inquiry Details</h3>
|
||||
<ul style="margin: 0; padding-left: 20px;">
|
||||
<li><strong>Inquiry ID:</strong> ${data.inquiryId}</li>
|
||||
<li><strong>Company:</strong> ${data.companyName}</li>
|
||||
<li><strong>Submitted:</strong> ${new Date().toLocaleDateString('en-IN')}</li>
|
||||
${data.productName ? `<li><strong>Product:</strong> ${data.productName}</li>` : ''}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3 style="color: #059669; margin: 30px 0 15px 0;">What happens next?</h3>
|
||||
<ol style="margin: 0 0 20px 0; padding-left: 20px;">
|
||||
<li style="margin-bottom: 10px;">Our procurement team will review your requirements within 2-4 business hours</li>
|
||||
<li style="margin-bottom: 10px;">We will prepare a detailed quotation including pricing, terms, and delivery schedules</li>
|
||||
<li style="margin-bottom: 10px;">A dedicated account manager will contact you within 24 hours to discuss your needs</li>
|
||||
<li style="margin-bottom: 10px;">We will provide samples if required and finalize the order details</li>
|
||||
</ol>
|
||||
|
||||
<div style="background: #fef3c7; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||
<p style="margin: 0; font-size: 14px; color: #92400e;">
|
||||
<strong>Priority Response:</strong> As a bulk inquiry, your request has been marked as high priority.
|
||||
Our business development team will reach out to you shortly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 style="color: #059669; margin: 30px 0 15px 0;">Need immediate assistance?</h3>
|
||||
<div style="background: #f0f9ff; padding: 20px; border-radius: 5px;">
|
||||
<p style="margin: 0 0 10px 0; font-size: 16px;">Contact our B2B Sales Team:</p>
|
||||
<ul style="margin: 0; padding-left: 20px; list-style: none;">
|
||||
<li style="margin-bottom: 5px;">📧 Email: ${process.env.ADMIN_EMAIL || 'info@padmajarice.com'}</li>
|
||||
<li style="margin-bottom: 5px;">📞 Phone: +91-9876543210 (B2B Hotline)</li>
|
||||
<li style="margin-bottom: 5px;">🕒 Business Hours: Monday - Saturday, 9:00 AM - 6:00 PM</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #f9fafb; padding: 20px; text-align: center; border-radius: 0 0 10px 10px; border-top: 1px solid #e5e7eb;">
|
||||
<p style="margin: 0; font-size: 14px; color: #6b7280;">
|
||||
This is an automated confirmation. Please do not reply to this email.
|
||||
</p>
|
||||
<p style="margin: 10px 0 0 0; font-size: 14px; color: #6b7280;">
|
||||
© 2024 Padmaaja Rasooi Pvt. Ltd. | Premium Rice & Grains
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
await this.sendEmail({
|
||||
to: data.customerEmail,
|
||||
subject: `B2B Inquiry Confirmation - ${data.companyName} | Padmaaja Rasooi`,
|
||||
html
|
||||
})
|
||||
}
|
||||
|
||||
// B2B Inquiry admin notification
|
||||
async sendB2BInquiryAdminNotification(data: {
|
||||
inquiryData: any
|
||||
inquiryId: string
|
||||
}) {
|
||||
const inquiry = data.inquiryData
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>New B2B Inquiry - Priority</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 700px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="margin: 0; font-size: 28px;">🚨 New B2B Inquiry - HIGH PRIORITY</h1>
|
||||
<p style="margin: 10px 0 0 0; font-size: 16px; opacity: 0.9;">Immediate attention required</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #fff; padding: 30px; border: 1px solid #e5e7eb;">
|
||||
<div style="background: #fef2f2; padding: 15px; border-left: 4px solid #dc2626; margin-bottom: 25px;">
|
||||
<p style="margin: 0; font-weight: bold; color: #dc2626;">
|
||||
⏰ Response Required Within 24 Hours
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 style="color: #059669; margin: 0 0 20px 0; border-bottom: 2px solid #059669; padding-bottom: 10px;">
|
||||
Company Information
|
||||
</h2>
|
||||
<div style="background: #f9fafb; padding: 20px; border-radius: 5px; margin-bottom: 25px;">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
||||
<div>
|
||||
<strong>Company:</strong><br>
|
||||
${inquiry.companyName}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Business Type:</strong><br>
|
||||
${inquiry.businessType}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Contact Person:</strong><br>
|
||||
${inquiry.contactPerson}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Designation:</strong><br>
|
||||
${inquiry.designation}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Email:</strong><br>
|
||||
<a href="mailto:${inquiry.email}" style="color: #059669;">${inquiry.email}</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Phone:</strong><br>
|
||||
<a href="tel:${inquiry.phone}" style="color: #059669;">${inquiry.phone}</a>
|
||||
</div>
|
||||
</div>
|
||||
${inquiry.gstNumber ? `
|
||||
<div style="margin-top: 15px;">
|
||||
<strong>GST Number:</strong> ${inquiry.gstNumber}
|
||||
</div>
|
||||
` : ''}
|
||||
<div style="margin-top: 15px;">
|
||||
<strong>Address:</strong><br>
|
||||
${inquiry.address}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${inquiry.productName ? `
|
||||
<h2 style="color: #059669; margin: 25px 0 20px 0; border-bottom: 2px solid #059669; padding-bottom: 10px;">
|
||||
Product Inquiry
|
||||
</h2>
|
||||
<div style="background: #f0f9ff; padding: 20px; border-radius: 5px; margin-bottom: 25px;">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
||||
<div>
|
||||
<strong>Product:</strong><br>
|
||||
${inquiry.productName}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Category:</strong><br>
|
||||
${inquiry.productCategory || 'N/A'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Listed Price:</strong><br>
|
||||
₹${inquiry.productPrice || 'N/A'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Product ID:</strong><br>
|
||||
${inquiry.productId || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<h2 style="color: #059669; margin: 25px 0 20px 0; border-bottom: 2px solid #059669; padding-bottom: 10px;">
|
||||
Requirements
|
||||
</h2>
|
||||
<div style="background: #fef7ff; padding: 20px; border-radius: 5px; margin-bottom: 25px;">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">
|
||||
<div>
|
||||
<strong>Quantity Required:</strong><br>
|
||||
${inquiry.quantityRequired} ${inquiry.quantityUnit}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Delivery Location:</strong><br>
|
||||
${inquiry.deliveryLocation}
|
||||
</div>
|
||||
</div>
|
||||
${inquiry.expectedDeliveryDate ? `
|
||||
<div style="margin-bottom: 15px;">
|
||||
<strong>Expected Delivery:</strong> ${inquiry.expectedDeliveryDate}
|
||||
</div>
|
||||
` : ''}
|
||||
<div>
|
||||
<strong>Detailed Requirements:</strong><br>
|
||||
<div style="background: white; padding: 15px; border-radius: 3px; margin-top: 5px; border: 1px solid #e5e7eb;">
|
||||
${inquiry.message.replace(/\n/g, '<br>')}
|
||||
</div>
|
||||
</div>
|
||||
${inquiry.hearAboutUs ? `
|
||||
<div style="margin-top: 15px;">
|
||||
<strong>How they heard about us:</strong> ${inquiry.hearAboutUs}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<h2 style="color: #059669; margin: 25px 0 20px 0; border-bottom: 2px solid #059669; padding-bottom: 10px;">
|
||||
Admin Actions
|
||||
</h2>
|
||||
<div style="background: #f0fdf4; padding: 20px; border-radius: 5px; text-align: center;">
|
||||
<p style="margin: 0 0 15px 0; font-weight: bold;">Inquiry ID: ${data.inquiryId}</p>
|
||||
<div style="margin: 15px 0;">
|
||||
<a href="${process.env.NEXTAUTH_URL || 'http://localhost:3000'}/admin/forms?filter=b2b_inquiry"
|
||||
style="background: #059669; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; margin: 0 10px; display: inline-block;">
|
||||
View in Admin Panel
|
||||
</a>
|
||||
<a href="mailto:${inquiry.email}?subject=Re: B2B Inquiry - ${inquiry.companyName}&body=Dear ${inquiry.contactPerson},%0D%0A%0D%0AThank you for your inquiry regarding bulk procurement. We have reviewed your requirements for ${inquiry.quantityRequired} ${inquiry.quantityUnit}..."
|
||||
style="background: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; margin: 0 10px; display: inline-block;">
|
||||
Reply to Customer
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #f9fafb; padding: 20px; text-align: center; border-radius: 0 0 10px 10px; border-top: 1px solid #e5e7eb;">
|
||||
<p style="margin: 0; font-size: 14px; color: #6b7280;">
|
||||
Submitted: ${new Date().toLocaleString('en-IN')} | Priority: HIGH
|
||||
</p>
|
||||
<p style="margin: 10px 0 0 0; font-size: 14px; color: #6b7280;">
|
||||
© 2024 Padmaaja Admin Panel
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
await this.sendEmail({
|
||||
to: process.env.ADMIN_EMAIL || 'info@padmajarice.com',
|
||||
subject: `🚨 URGENT: New B2B Inquiry from ${inquiry.companyName} - ${inquiry.quantityRequired} ${inquiry.quantityUnit}`,
|
||||
html
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const emailService = new EmailServiceClass()
|
||||
|
||||
// Export the class properly without circular reference
|
||||
export const EmailService = EmailServiceClass
|
||||
export default EmailServiceClass
|
||||
62
lib/json-ld-helpers.ts
Normal file
62
lib/json-ld-helpers.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* JSON-LD Helper Utilities for Next.js
|
||||
*
|
||||
* Following Next.js best practices for structured data:
|
||||
* - Sanitizes output to prevent XSS attacks
|
||||
* - Uses TypeScript with schema-dts for type safety
|
||||
* - Implements proper escaping of < characters
|
||||
*/
|
||||
|
||||
import { WithContext, Thing } from 'schema-dts'
|
||||
|
||||
/**
|
||||
* Safely stringify JSON-LD data with XSS protection
|
||||
* Replaces < with \u003c to prevent script injection
|
||||
*/
|
||||
export function safeJsonLdStringify(data: WithContext<Thing> | any): string {
|
||||
return JSON.stringify(data).replace(/</g, '\\u003c')
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSON-LD script tag props object
|
||||
* Use with dangerouslySetInnerHTML in Next.js components
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <script
|
||||
* type="application/ld+json"
|
||||
* dangerouslySetInnerHTML={createJsonLdScriptProps(jsonLd)}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export function createJsonLdScriptProps(data: WithContext<Thing> | any) {
|
||||
return {
|
||||
__html: safeJsonLdStringify(data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if data is valid JSON-LD
|
||||
*/
|
||||
export function isValidJsonLd(data: any): data is WithContext<Thing> {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'@context' in data &&
|
||||
'@type' in data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge multiple JSON-LD objects into a graph
|
||||
* Useful when you need to include multiple structured data types on one page
|
||||
*/
|
||||
export function mergeJsonLd(...items: Array<WithContext<Thing>>): { '@context': string; '@graph': any[] } {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': items.map(item => {
|
||||
const { '@context': _, ...rest } = item as any
|
||||
return rest
|
||||
})
|
||||
}
|
||||
}
|
||||
224
lib/metadata.ts
Normal file
224
lib/metadata.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { Metadata } from 'next'
|
||||
|
||||
interface PageMetadataParams {
|
||||
title?: string
|
||||
description?: string
|
||||
keywords?: string[]
|
||||
image?: string
|
||||
url?: string
|
||||
type?: 'website' | 'article' | 'product'
|
||||
publishedTime?: string
|
||||
modifiedTime?: string
|
||||
author?: string
|
||||
noIndex?: boolean
|
||||
}
|
||||
|
||||
export function generatePageMetadata({
|
||||
title = 'Padmaaja Rasooi - Premium Rice Products & Quality Grains',
|
||||
description = 'Experience the finest quality rice with Padmaaja Rasooi. Discover our premium rice products and quality grains sourced directly from farmers.',
|
||||
keywords = ['padmaaja rasooi', 'premium rice', 'quality rice', 'quality grains', 'organic rice', 'farm fresh rice'],
|
||||
image = '/images/og-image.png',
|
||||
url = '/',
|
||||
type = 'website',
|
||||
publishedTime,
|
||||
modifiedTime,
|
||||
author = 'Padmaaja Rasooi Team',
|
||||
noIndex = false
|
||||
}: PageMetadataParams): Metadata {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_URL || 'https://padmaajarasooi.com'
|
||||
const fullUrl = `${baseUrl}${url}`
|
||||
const fullImageUrl = image.startsWith('http') ? image : `${baseUrl}${image}`
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
keywords,
|
||||
authors: [{ name: author }],
|
||||
creator: 'Padmaaja Rasooi',
|
||||
publisher: 'Padmaaja Rasooi',
|
||||
metadataBase: new URL(baseUrl),
|
||||
alternates: {
|
||||
canonical: url,
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: fullUrl,
|
||||
siteName: 'Padmaaja Rasooi',
|
||||
locale: 'en_US',
|
||||
type: type as any,
|
||||
publishedTime,
|
||||
modifiedTime,
|
||||
authors: [author],
|
||||
images: [
|
||||
{
|
||||
url: fullImageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: title
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title,
|
||||
description,
|
||||
images: [fullImageUrl],
|
||||
site: '@padmaajarasooi',
|
||||
creator: '@padmaajarasooi'
|
||||
},
|
||||
robots: noIndex ? {
|
||||
index: false,
|
||||
follow: false,
|
||||
} : {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
'max-video-preview': -1,
|
||||
'max-image-preview': 'large',
|
||||
'max-snippet': -1,
|
||||
},
|
||||
},
|
||||
verification: {
|
||||
google: 'your-google-site-verification', // Add your actual verification code
|
||||
// Note: Next.js only supports google and yahoo verification in the verification object
|
||||
// For other search engines, use meta tags in the head section
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-configured metadata for common pages
|
||||
export const MetadataConfigs = {
|
||||
home: (): Metadata => generatePageMetadata({
|
||||
title: 'Padmaaja Rasooi - Premium Rice Products & Quality Grains',
|
||||
description: 'Experience the finest quality rice with Padmaaja Rasooi. Discover our premium rice products and quality grains sourced directly from farmers across India.',
|
||||
keywords: ['padmaaja rasooi', 'premium rice', 'quality rice', 'quality grains', 'organic rice', 'basmati rice', 'rice supplier India', 'farm fresh rice', 'premium grains'],
|
||||
url: '/',
|
||||
image: '/images/home-og.png'
|
||||
}),
|
||||
|
||||
products: (): Metadata => generatePageMetadata({
|
||||
title: 'Premium Rice Products | Padmaaja Rasooi - Quality Rice Collection',
|
||||
description: 'Explore our extensive collection of premium quality rice varieties. From organic basmati to traditional rice, find the perfect rice for your culinary needs.',
|
||||
keywords: ['premium rice products', 'organic rice', 'basmati rice', 'rice varieties', 'quality rice', 'rice collection', 'padmaaja rasooi products'],
|
||||
url: '/products',
|
||||
image: '/images/products-og.png'
|
||||
}),
|
||||
|
||||
about: (): Metadata => generatePageMetadata({
|
||||
title: 'About Padmaaja Rasooi | Quality Rice Heritage & Sustainable Farming',
|
||||
description: 'Learn about Padmaaja Rasooi\'s commitment to quality rice products, sustainable farming practices, and our premium grain sourcing from local farmers.',
|
||||
keywords: ['about padmaaja rasooi', 'rice company history', 'quality standards', 'sustainable farming', 'rice heritage', 'company values'],
|
||||
url: '/about',
|
||||
image: '/images/about-og.png'
|
||||
}),
|
||||
|
||||
contact: (): Metadata => generatePageMetadata({
|
||||
title: 'Contact Padmaaja Rasooi | Rice Supplier & Customer Support',
|
||||
description: 'Get in touch with Padmaaja Rasooi for premium rice products, wholesale inquiries, bulk orders, or customer support.',
|
||||
keywords: ['contact padmaaja rasooi', 'rice supplier contact', 'wholesale inquiry', 'customer support', 'wholesale rice', 'bulk rice orders'],
|
||||
url: '/contact',
|
||||
image: '/images/contact-og.png'
|
||||
}),
|
||||
|
||||
qualityStandards: (): Metadata => generatePageMetadata({
|
||||
title: 'Quality Standards | Padmaaja Rasooi - Premium Rice Quality Assurance',
|
||||
description: 'Discover our rigorous quality standards and testing procedures that ensure every grain of Padmaaja Rasooi rice meets the highest quality benchmarks.',
|
||||
keywords: ['rice quality standards', 'quality assurance', 'rice testing', 'premium quality', 'food safety', 'padmaaja rasooi quality'],
|
||||
url: '/about/quality',
|
||||
image: '/images/quality-og.png'
|
||||
}),
|
||||
|
||||
privateLabel: (): Metadata => generatePageMetadata({
|
||||
title: 'Private Label Services | Padmaaja Rasooi - Custom Rice Branding',
|
||||
description: 'Explore our private label rice services. Partner with Padmaaja Rasooi to create your own branded premium rice products with our quality assurance.',
|
||||
keywords: ['private label rice', 'custom rice branding', 'white label rice', 'rice manufacturing', 'private label services', 'branded rice'],
|
||||
url: '/private-label',
|
||||
image: '/images/private-label-og.png'
|
||||
}),
|
||||
|
||||
exportServices: (): Metadata => generatePageMetadata({
|
||||
title: 'Export Services | Padmaaja Rasooi - International Rice Supply',
|
||||
description: 'Padmaaja Rasooi export services for international rice supply. Quality Indian rice exported globally with proper certifications and packaging.',
|
||||
keywords: ['rice export', 'international rice supply', 'rice exporter India', 'global rice supply', 'export services', 'international trade'],
|
||||
url: '/export',
|
||||
image: '/images/export-og.png'
|
||||
}),
|
||||
|
||||
privacyPolicy: (): Metadata => generatePageMetadata({
|
||||
title: 'Privacy Policy | Padmaaja Rasooi - Data Protection & Privacy Rights',
|
||||
description: 'Read our comprehensive privacy policy to understand how Padmaaja Rasooi protects your personal information and respects your privacy rights.',
|
||||
keywords: ['privacy policy', 'data protection', 'personal information', 'GDPR compliance', 'privacy rights', 'padmaaja rasooi'],
|
||||
url: '/legal/privacy-policy',
|
||||
image: '/images/legal-og.png'
|
||||
}),
|
||||
|
||||
termsOfService: (): Metadata => generatePageMetadata({
|
||||
title: 'Terms of Service | Padmaaja Rasooi - Service Agreement & Policies',
|
||||
description: 'Read our comprehensive terms of service covering product purchases, customer responsibilities, and service policies for Padmaaja Rasooi.',
|
||||
keywords: ['terms of service', 'service agreement', 'purchase terms', 'legal terms', 'conditions', 'padmaaja rasooi'],
|
||||
url: '/legal/terms-of-service',
|
||||
image: '/images/legal-og.png'
|
||||
}),
|
||||
|
||||
refundPolicy: (): Metadata => generatePageMetadata({
|
||||
title: 'Refund Policy | Padmaaja Rasooi - Returns & Refund Guidelines',
|
||||
description: 'Comprehensive refund and return policy for Padmaaja Rasooi rice products. Learn about eligibility, process, and timelines for returns.',
|
||||
keywords: ['refund policy', 'return policy', 'money back guarantee', 'product returns', 'refund process', 'padmaaja rasooi'],
|
||||
url: '/legal/refund-policy',
|
||||
image: '/images/legal-og.png'
|
||||
}),
|
||||
|
||||
dashboard: (): Metadata => generatePageMetadata({
|
||||
title: 'Dashboard | Padmaaja Rasooi - Account Management',
|
||||
description: 'Access your Padmaaja Rasooi dashboard to manage your account, track orders, view purchase history, and manage your profile.',
|
||||
keywords: ['customer dashboard', 'account management', 'order tracking', 'purchase history', 'padmaaja rasooi'],
|
||||
url: '/dashboard',
|
||||
image: '/images/dashboard-og.png',
|
||||
noIndex: true // Private area
|
||||
}),
|
||||
|
||||
adminPanel: (): Metadata => generatePageMetadata({
|
||||
title: 'Admin Panel | Padmaaja Rasooi - Business Administration',
|
||||
description: 'Administrative panel for managing Padmaaja Rasooi business operations, users, products, and system settings.',
|
||||
keywords: ['admin panel', 'business administration', 'system management', 'padmaaja rasooi'],
|
||||
url: '/admin',
|
||||
image: '/images/admin-og.png',
|
||||
noIndex: true // Private area
|
||||
})
|
||||
}
|
||||
|
||||
// Dynamic metadata generators
|
||||
export function generateProductMetadata(product: {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
price: number
|
||||
category: string
|
||||
images: string[]
|
||||
}): Metadata {
|
||||
return generatePageMetadata({
|
||||
title: `${product.name} | Premium Rice | Padmaaja Rasooi`,
|
||||
description: `${product.description} Premium quality ${product.category.toLowerCase()} rice from Padmaaja Rasooi. Price: ₹${product.price}`,
|
||||
keywords: [product.name.toLowerCase(), product.category.toLowerCase(), 'premium rice', 'quality rice', 'padmaaja rasooi'],
|
||||
url: `/products/${product.id}`,
|
||||
image: product.images[0] || '/images/product-default-og.png',
|
||||
type: 'product'
|
||||
})
|
||||
}
|
||||
|
||||
export function generateCategoryMetadata(category: {
|
||||
name: string
|
||||
description: string
|
||||
productCount: number
|
||||
}): Metadata {
|
||||
return generatePageMetadata({
|
||||
title: `${category.name} Rice | Premium Quality | Padmaaja Rasooi`,
|
||||
description: `${category.description} Explore ${category.productCount}+ premium ${category.name.toLowerCase()} rice varieties from Padmaaja Rasooi.`,
|
||||
keywords: [category.name.toLowerCase(), 'rice category', 'premium rice', 'quality rice', 'padmaaja rasooi'],
|
||||
url: `/products/category/${category.name.toLowerCase().replace(/\s+/g, '-')}`,
|
||||
image: `/images/category-${category.name.toLowerCase().replace(/\s+/g, '-')}-og.png`
|
||||
})
|
||||
}
|
||||
72
lib/performance-monitor.ts
Normal file
72
lib/performance-monitor.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// Performance monitoring utility
|
||||
export class PerformanceMonitor {
|
||||
private static marks: Map<string, number> = new Map()
|
||||
|
||||
static mark(name: string) {
|
||||
if (typeof window !== 'undefined' && window.performance) {
|
||||
const now = performance.now()
|
||||
this.marks.set(name, now)
|
||||
performance.mark(name)
|
||||
}
|
||||
}
|
||||
|
||||
static measure(name: string, startMark: string, endMark?: string) {
|
||||
if (typeof window !== 'undefined' && window.performance) {
|
||||
try {
|
||||
if (endMark) {
|
||||
performance.measure(name, startMark, endMark)
|
||||
} else {
|
||||
performance.measure(name, startMark)
|
||||
}
|
||||
|
||||
const measure = performance.getEntriesByName(name, 'measure')[0]
|
||||
console.log(`⚡ ${name}: ${measure.duration.toFixed(2)}ms`)
|
||||
return measure.duration
|
||||
} catch (error) {
|
||||
console.warn('Performance measurement failed:', error)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
static getMetrics() {
|
||||
if (typeof window !== 'undefined' && window.performance) {
|
||||
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
|
||||
|
||||
return {
|
||||
// Core Web Vitals approximations
|
||||
FCP: navigation.responseEnd - navigation.fetchStart,
|
||||
LCP: navigation.loadEventEnd - navigation.fetchStart,
|
||||
TTFB: navigation.responseStart - navigation.requestStart,
|
||||
|
||||
// Loading metrics
|
||||
DOMContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
|
||||
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
|
||||
|
||||
// Network metrics
|
||||
DNSLookup: navigation.domainLookupEnd - navigation.domainLookupStart,
|
||||
TCPConnection: navigation.connectEnd - navigation.connectStart,
|
||||
|
||||
// Page lifecycle
|
||||
totalTime: navigation.loadEventEnd - navigation.fetchStart
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static logMetrics() {
|
||||
const metrics = this.getMetrics()
|
||||
if (metrics) {
|
||||
console.group('📊 Performance Metrics')
|
||||
console.log('🎨 First Contentful Paint (approx):', metrics.FCP.toFixed(2) + 'ms')
|
||||
console.log('🖼️ Largest Contentful Paint (approx):', metrics.LCP.toFixed(2) + 'ms')
|
||||
console.log('⚡ Time to First Byte:', metrics.TTFB.toFixed(2) + 'ms')
|
||||
console.log('📄 DOM Content Loaded:', metrics.DOMContentLoaded.toFixed(2) + 'ms')
|
||||
console.log('✅ Load Complete:', metrics.loadComplete.toFixed(2) + 'ms')
|
||||
console.log('🌐 DNS Lookup:', metrics.DNSLookup.toFixed(2) + 'ms')
|
||||
console.log('🔗 TCP Connection:', metrics.TCPConnection.toFixed(2) + 'ms')
|
||||
console.log('⏱️ Total Page Load:', metrics.totalTime.toFixed(2) + 'ms')
|
||||
console.groupEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
173
lib/pricing.ts
Normal file
173
lib/pricing.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export interface PricingOptions {
|
||||
userId?: string
|
||||
userRole?: string
|
||||
quantity?: number
|
||||
isWholesaler?: boolean
|
||||
}
|
||||
|
||||
export class PricingService {
|
||||
// Get effective price for a product based on user role and quantity
|
||||
static async getEffectivePrice(
|
||||
productId: string,
|
||||
quantity: number = 1,
|
||||
options: PricingOptions = {}
|
||||
): Promise<{
|
||||
originalPrice: number
|
||||
finalPrice: number
|
||||
discount: number
|
||||
discountPercentage: number
|
||||
isWholesalePrice: boolean
|
||||
}> {
|
||||
// Get the product
|
||||
const product = await prisma.product.findUnique({
|
||||
where: { id: productId }
|
||||
})
|
||||
|
||||
if (!product) {
|
||||
throw new Error('Product not found')
|
||||
}
|
||||
|
||||
const originalPrice = product.price
|
||||
let finalPrice = originalPrice
|
||||
let discount = 0
|
||||
let discountPercentage = 0
|
||||
let isWholesalePrice = false
|
||||
|
||||
// Apply product discount first
|
||||
if (product.discount > 0) {
|
||||
discount = (originalPrice * product.discount) / 100
|
||||
finalPrice = originalPrice - discount
|
||||
discountPercentage = product.discount
|
||||
}
|
||||
|
||||
// Check if user is a wholesaler and quantity qualifies for bulk discount
|
||||
if (options.userRole === 'WHOLESALER' && this.isQualifyingBulkOrder(quantity)) {
|
||||
// Apply 25% wholesale discount on top of existing discount
|
||||
const wholesaleDiscount = (originalPrice * 25) / 100
|
||||
|
||||
// Use the better discount (wholesale or product discount)
|
||||
if (wholesaleDiscount > discount) {
|
||||
discount = wholesaleDiscount
|
||||
finalPrice = originalPrice - discount
|
||||
discountPercentage = 25
|
||||
isWholesalePrice = true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
originalPrice,
|
||||
finalPrice: Math.max(finalPrice, 0), // Ensure price doesn't go negative
|
||||
discount,
|
||||
discountPercentage,
|
||||
isWholesalePrice
|
||||
}
|
||||
}
|
||||
|
||||
// Check if quantity qualifies for bulk order discount
|
||||
static isQualifyingBulkOrder(quantity: number): boolean {
|
||||
// Define minimum quantity for bulk orders (e.g., 10 or more items)
|
||||
const BULK_ORDER_MIN_QUANTITY = 10
|
||||
return quantity >= BULK_ORDER_MIN_QUANTITY
|
||||
}
|
||||
|
||||
// Calculate cart total with wholesale discounts
|
||||
static async calculateCartTotal(
|
||||
cartItems: Array<{
|
||||
productId: string
|
||||
quantity: number
|
||||
}>,
|
||||
options: PricingOptions = {}
|
||||
): Promise<{
|
||||
subtotal: number
|
||||
totalDiscount: number
|
||||
finalTotal: number
|
||||
items: Array<{
|
||||
productId: string
|
||||
quantity: number
|
||||
originalPrice: number
|
||||
finalPrice: number
|
||||
discount: number
|
||||
discountPercentage: number
|
||||
isWholesalePrice: boolean
|
||||
lineTotal: number
|
||||
}>
|
||||
hasWholesaleDiscount: boolean
|
||||
}> {
|
||||
let subtotal = 0
|
||||
let totalDiscount = 0
|
||||
let finalTotal = 0
|
||||
let hasWholesaleDiscount = false
|
||||
const items = []
|
||||
|
||||
for (const item of cartItems) {
|
||||
const pricing = await this.getEffectivePrice(
|
||||
item.productId,
|
||||
item.quantity,
|
||||
options
|
||||
)
|
||||
|
||||
const lineTotal = pricing.finalPrice * item.quantity
|
||||
const lineDiscount = pricing.discount * item.quantity
|
||||
|
||||
subtotal += pricing.originalPrice * item.quantity
|
||||
totalDiscount += lineDiscount
|
||||
finalTotal += lineTotal
|
||||
|
||||
if (pricing.isWholesalePrice) {
|
||||
hasWholesaleDiscount = true
|
||||
}
|
||||
|
||||
items.push({
|
||||
productId: item.productId,
|
||||
quantity: item.quantity,
|
||||
originalPrice: pricing.originalPrice,
|
||||
finalPrice: pricing.finalPrice,
|
||||
discount: pricing.discount,
|
||||
discountPercentage: pricing.discountPercentage,
|
||||
isWholesalePrice: pricing.isWholesalePrice,
|
||||
lineTotal
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subtotal,
|
||||
totalDiscount,
|
||||
finalTotal,
|
||||
items,
|
||||
hasWholesaleDiscount
|
||||
}
|
||||
}
|
||||
|
||||
// Get user role for pricing calculations
|
||||
static async getUserRole(userId: string): Promise<string | null> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { role: true }
|
||||
})
|
||||
|
||||
return user?.role || null
|
||||
}
|
||||
|
||||
// Get wholesale discount info for display
|
||||
static getWholesaleDiscountInfo() {
|
||||
return {
|
||||
discountPercentage: 25,
|
||||
minQuantity: 10,
|
||||
description: 'Get 25% off on bulk orders (10+ items) as a registered wholesaler'
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user is eligible for wholesale pricing
|
||||
static async isWholesaler(userId: string): Promise<boolean> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { role: true, isActive: true }
|
||||
})
|
||||
|
||||
return user?.role === 'WHOLESALER' && user?.isActive === true
|
||||
}
|
||||
}
|
||||
|
||||
export default PricingService
|
||||
11
lib/prisma.ts
Normal file
11
lib/prisma.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined
|
||||
}
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
})
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
||||
113
lib/pwa.ts
Normal file
113
lib/pwa.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
// PWA utility functions
|
||||
|
||||
export const isPWA = (): boolean => {
|
||||
return window.matchMedia('(display-mode: standalone)').matches ||
|
||||
(window.navigator as any).standalone === true
|
||||
}
|
||||
|
||||
export const isOnline = (): boolean => {
|
||||
return navigator.onLine
|
||||
}
|
||||
|
||||
export const addBeforeInstallPromptListener = (callback: (event: any) => void) => {
|
||||
window.addEventListener('beforeinstallprompt', callback)
|
||||
}
|
||||
|
||||
export const removeBeforeInstallPromptListener = (callback: (event: any) => void) => {
|
||||
window.removeEventListener('beforeinstallprompt', callback)
|
||||
}
|
||||
|
||||
// Request notification permission
|
||||
export const requestNotificationPermission = async (): Promise<NotificationPermission> => {
|
||||
if (!('Notification' in window)) {
|
||||
throw new Error('This browser does not support notifications')
|
||||
}
|
||||
|
||||
const permission = await Notification.requestPermission()
|
||||
return permission
|
||||
}
|
||||
|
||||
// Show local notification
|
||||
export const showNotification = (title: string, options?: NotificationOptions) => {
|
||||
if ('serviceWorker' in navigator && 'Notification' in window) {
|
||||
if (Notification.permission === 'granted') {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.showNotification(title, {
|
||||
icon: '/icons/icon-192x192.png',
|
||||
badge: '/icons/badge-72x72.png',
|
||||
...options
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show commission notification
|
||||
export const showCommissionNotification = (amount: number, level: number) => {
|
||||
showNotification('Commission Earned! 💰', {
|
||||
body: `You earned ₹${amount.toFixed(2)} from Level ${level} commission`,
|
||||
icon: '/icons/icon-192x192.png',
|
||||
tag: 'commission',
|
||||
requireInteraction: true
|
||||
})
|
||||
}
|
||||
|
||||
// Show order notification
|
||||
export const showOrderNotification = (orderTotal: number) => {
|
||||
showNotification('Order Confirmed! 🎉', {
|
||||
body: `Your order of ₹${orderTotal.toFixed(2)} has been confirmed`,
|
||||
icon: '/icons/icon-192x192.png',
|
||||
tag: 'order'
|
||||
})
|
||||
}
|
||||
|
||||
// Cache management
|
||||
export const clearCache = async (): Promise<void> => {
|
||||
if ('caches' in window) {
|
||||
const cacheNames = await caches.keys()
|
||||
await Promise.all(
|
||||
cacheNames.map(cacheName => caches.delete(cacheName))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Background sync registration
|
||||
export const registerBackgroundSync = (tag: string): void => {
|
||||
if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
return (registration as any).sync.register(tag)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Store data for offline use
|
||||
export const storeOfflineData = async (key: string, data: any): Promise<void> => {
|
||||
if ('caches' in window) {
|
||||
const cache = await caches.open('padmaaja-rasooi-dynamic-v1')
|
||||
const response = new Response(JSON.stringify(data), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
await cache.put(`/offline-data/${key}`, response)
|
||||
}
|
||||
}
|
||||
|
||||
// Get stored offline data
|
||||
export const getOfflineData = async (key: string): Promise<any> => {
|
||||
if ('caches' in window) {
|
||||
const cache = await caches.open('padmaaja-rasooi-dynamic-v1')
|
||||
const response = await cache.match(`/offline-data/${key}`)
|
||||
return response ? response.json() : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Install prompt handling
|
||||
export const installApp = async (deferredPrompt: any): Promise<boolean> => {
|
||||
if (!deferredPrompt) return false
|
||||
|
||||
deferredPrompt.prompt()
|
||||
const { outcome } = await deferredPrompt.userChoice
|
||||
return outcome === 'accepted'
|
||||
}
|
||||
|
||||
|
||||
206
lib/ranks.ts
Normal file
206
lib/ranks.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { emailService } from '@/lib/email'
|
||||
|
||||
interface RankCalculation {
|
||||
totalReferrals: number
|
||||
salesVolume: number
|
||||
teamVolume: number
|
||||
}
|
||||
|
||||
export class RankSystem {
|
||||
async calculateUserMetrics(userId: string): Promise<RankCalculation> {
|
||||
// Get direct referrals count
|
||||
const totalReferrals = await prisma.user.count({
|
||||
where: { referrerId: userId }
|
||||
})
|
||||
|
||||
// Get user's personal sales volume (last 30 days)
|
||||
const salesVolumeResult = await prisma.order.aggregate({
|
||||
where: {
|
||||
userId,
|
||||
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] },
|
||||
createdAt: {
|
||||
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // Last 30 days
|
||||
}
|
||||
},
|
||||
_sum: { total: true }
|
||||
})
|
||||
|
||||
// Get team sales volume (referrals' sales in last 30 days)
|
||||
const referralIds = await prisma.user.findMany({
|
||||
where: { referrerId: userId },
|
||||
select: { id: true }
|
||||
}).then(users => users.map(u => u.id))
|
||||
|
||||
const teamVolumeResult = await prisma.order.aggregate({
|
||||
where: {
|
||||
userId: { in: referralIds },
|
||||
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] },
|
||||
createdAt: {
|
||||
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
||||
}
|
||||
},
|
||||
_sum: { total: true }
|
||||
})
|
||||
|
||||
return {
|
||||
totalReferrals,
|
||||
salesVolume: salesVolumeResult._sum.total || 0,
|
||||
teamVolume: teamVolumeResult._sum.total || 0
|
||||
}
|
||||
}
|
||||
|
||||
async getEligibleRank(metrics: RankCalculation) {
|
||||
const ranks = await prisma.rank.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: { order: 'desc' }
|
||||
})
|
||||
|
||||
// Find highest rank user qualifies for
|
||||
for (const rank of ranks) {
|
||||
if (
|
||||
metrics.totalReferrals >= rank.minReferrals &&
|
||||
metrics.salesVolume >= rank.minSalesVolume &&
|
||||
metrics.teamVolume >= rank.minTeamVolume
|
||||
) {
|
||||
return rank
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async updateUserRank(userId: string) {
|
||||
try {
|
||||
const metrics = await this.calculateUserMetrics(userId)
|
||||
const eligibleRank = await this.getEligibleRank(metrics)
|
||||
|
||||
if (!eligibleRank) return null
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { currentRank: true }
|
||||
})
|
||||
|
||||
if (!user) return null
|
||||
|
||||
// Check if this is a rank upgrade
|
||||
const isUpgrade = !user.currentRank || eligibleRank.order > user.currentRank.order
|
||||
|
||||
if (isUpgrade) {
|
||||
// Update user's current rank
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { currentRankId: eligibleRank.id }
|
||||
})
|
||||
|
||||
// Record achievement
|
||||
await prisma.rankAchievement.upsert({
|
||||
where: {
|
||||
userId_rankId: {
|
||||
userId,
|
||||
rankId: eligibleRank.id
|
||||
}
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
userId,
|
||||
rankId: eligibleRank.id
|
||||
}
|
||||
})
|
||||
|
||||
// Send achievement email
|
||||
try {
|
||||
await emailService.sendRankAchievement(user.email, user.name || 'User', eligibleRank)
|
||||
} catch (emailError) {
|
||||
console.error('Failed to send rank achievement email:', emailError)
|
||||
}
|
||||
|
||||
console.log(`User ${user.email} achieved rank: ${eligibleRank.name}`)
|
||||
return eligibleRank
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('Error updating user rank:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async initializeDefaultRanks() {
|
||||
const existingRanks = await prisma.rank.count()
|
||||
|
||||
if (existingRanks === 0) {
|
||||
const defaultRanks = [
|
||||
{
|
||||
name: 'Bronze',
|
||||
description: 'Welcome to the journey!',
|
||||
minReferrals: 0,
|
||||
minSalesVolume: 0,
|
||||
minTeamVolume: 0,
|
||||
commissionMultiplier: 1.0,
|
||||
benefits: ['Basic commission rates', 'Access to all products'],
|
||||
color: '#CD7F32',
|
||||
icon: '🥉',
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
name: 'Silver',
|
||||
description: 'Building momentum!',
|
||||
minReferrals: 3,
|
||||
minSalesVolume: 5000,
|
||||
minTeamVolume: 10000,
|
||||
commissionMultiplier: 1.2,
|
||||
benefits: ['20% commission bonus', 'Priority support', 'Monthly bonuses'],
|
||||
color: '#C0C0C0',
|
||||
icon: '🥈',
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
name: 'Gold',
|
||||
description: 'Excellent performance!',
|
||||
minReferrals: 5,
|
||||
minSalesVolume: 15000,
|
||||
minTeamVolume: 30000,
|
||||
commissionMultiplier: 1.5,
|
||||
benefits: ['50% commission bonus', 'VIP support', 'Exclusive events'],
|
||||
color: '#FFD700',
|
||||
icon: '🥇',
|
||||
order: 3
|
||||
},
|
||||
{
|
||||
name: 'Platinum',
|
||||
description: 'Elite achiever!',
|
||||
minReferrals: 10,
|
||||
minSalesVolume: 30000,
|
||||
minTeamVolume: 75000,
|
||||
commissionMultiplier: 2.0,
|
||||
benefits: ['100% commission bonus', 'Personal coach', 'Leadership trips'],
|
||||
color: '#E5E4E2',
|
||||
icon: '💎',
|
||||
order: 4
|
||||
},
|
||||
{
|
||||
name: 'Diamond',
|
||||
description: 'Ultimate success!',
|
||||
minReferrals: 15,
|
||||
minSalesVolume: 50000,
|
||||
minTeamVolume: 150000,
|
||||
commissionMultiplier: 3.0,
|
||||
benefits: ['200% commission bonus', 'Revenue sharing', 'Executive benefits'],
|
||||
color: '#B9F2FF',
|
||||
icon: '💎',
|
||||
order: 5
|
||||
}
|
||||
]
|
||||
|
||||
await prisma.rank.createMany({
|
||||
data: defaultRanks
|
||||
})
|
||||
|
||||
console.log('Default ranks initialized')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const rankSystem = new RankSystem()
|
||||
65
lib/razorpay.ts
Normal file
65
lib/razorpay.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import Razorpay from 'razorpay'
|
||||
|
||||
const isProduction = process.env.RAZORPAY_ENV === 'production'
|
||||
|
||||
export const getRazorpayConfig = () => {
|
||||
const keyId = isProduction
|
||||
? process.env.RAZORPAY_LIVE_KEY_ID
|
||||
: process.env.RAZORPAY_TEST_KEY_ID
|
||||
|
||||
const keySecret = isProduction
|
||||
? process.env.RAZORPAY_LIVE_KEY_SECRET
|
||||
: process.env.RAZORPAY_TEST_KEY_SECRET
|
||||
|
||||
if (!keyId || !keySecret) {
|
||||
throw new Error(`Missing Razorpay ${isProduction ? 'live' : 'test'} credentials`)
|
||||
}
|
||||
|
||||
return {
|
||||
keyId,
|
||||
keySecret,
|
||||
isProduction
|
||||
}
|
||||
}
|
||||
|
||||
export const createRazorpayInstance = () => {
|
||||
const config = getRazorpayConfig()
|
||||
|
||||
return new Razorpay({
|
||||
key_id: config.keyId,
|
||||
key_secret: config.keySecret,
|
||||
})
|
||||
}
|
||||
|
||||
export const getRazorpayKeyId = () => {
|
||||
const config = getRazorpayConfig()
|
||||
return config.keyId
|
||||
}
|
||||
|
||||
export const createOrder = async (amount: number, orderId: string): Promise<any> => {
|
||||
try {
|
||||
const order = await createRazorpayInstance().orders.create({
|
||||
amount: Math.round(amount * 100), // Convert to paise
|
||||
currency: 'INR',
|
||||
receipt: orderId,
|
||||
payment_capture: true,
|
||||
})
|
||||
return order
|
||||
} catch (error) {
|
||||
console.error('Error creating Razorpay order:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const verifyPayment = (
|
||||
razorpayOrderId: string,
|
||||
razorpayPaymentId: string,
|
||||
razorpaySignature: string
|
||||
) => {
|
||||
const crypto = require('crypto')
|
||||
const hmac = crypto.createHmac('sha256', process.env.RAZORPAY_KEY_SECRET!)
|
||||
hmac.update(`${razorpayOrderId}|${razorpayPaymentId}`)
|
||||
const generatedSignature = hmac.digest('hex')
|
||||
|
||||
return generatedSignature === razorpaySignature
|
||||
}
|
||||
376
lib/recipe-data.ts
Normal file
376
lib/recipe-data.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
export interface Recipe {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
image: string
|
||||
cookTime: string
|
||||
servings: number
|
||||
difficulty: 'Easy' | 'Medium' | 'Hard'
|
||||
category: string
|
||||
ingredients: string[]
|
||||
instructions: string[]
|
||||
tips?: string[]
|
||||
nutritionInfo?: {
|
||||
calories: number
|
||||
protein: string
|
||||
carbs: string
|
||||
fat: string
|
||||
}
|
||||
}
|
||||
|
||||
export const riceRecipes: Recipe[] = [
|
||||
{
|
||||
id: 'vegetable-mushroom-pulao',
|
||||
name: 'Vegetable Mushroom Pulao',
|
||||
description: 'A fragrant and flavorful rice dish with mixed vegetables and fresh mushrooms, perfect for a wholesome meal.',
|
||||
image: 'https://4m5m4tx28rtva30c.public.blob.vercel-storage.com/media/2025-09-07/vegetable-mushroom-pulao',
|
||||
cookTime: '30 mins',
|
||||
servings: 4,
|
||||
difficulty: 'Medium',
|
||||
category: 'Main Course',
|
||||
ingredients: [
|
||||
'2 cups Basmati rice',
|
||||
'200g mixed mushrooms, sliced',
|
||||
'1 large onion, sliced',
|
||||
'1 cup mixed vegetables (carrots, beans, peas)',
|
||||
'3-4 green cardamom pods',
|
||||
'2 bay leaves',
|
||||
'1 cinnamon stick',
|
||||
'4 cloves',
|
||||
'2 tbsp ghee or oil',
|
||||
'1 tsp cumin seeds',
|
||||
'1 tsp ginger-garlic paste',
|
||||
'2 green chilies, slit',
|
||||
'1/2 tsp turmeric powder',
|
||||
'1 tsp garam masala',
|
||||
'Salt to taste',
|
||||
'3 cups water or vegetable stock',
|
||||
'Fresh coriander leaves for garnish'
|
||||
],
|
||||
instructions: [
|
||||
'Wash and soak basmati rice for 30 minutes, then drain.',
|
||||
'Heat ghee in a heavy-bottomed pot. Add whole spices (cardamom, bay leaves, cinnamon, cloves) and cumin seeds.',
|
||||
'Add sliced onions and sauté until golden brown.',
|
||||
'Add ginger-garlic paste and green chilies. Cook for 1 minute.',
|
||||
'Add mushrooms and cook until they release water and become tender.',
|
||||
'Add mixed vegetables, turmeric, and salt. Cook for 3-4 minutes.',
|
||||
'Add the soaked rice and gently mix. Add garam masala.',
|
||||
'Pour hot water or stock. The liquid should be 1 inch above the rice level.',
|
||||
'Bring to a boil, then reduce heat to low, cover and cook for 18-20 minutes.',
|
||||
'Let it rest for 5 minutes before opening. Gently mix and garnish with coriander.',
|
||||
'Serve hot with raita and pickle.'
|
||||
],
|
||||
tips: [
|
||||
'Use aged basmati rice for best results',
|
||||
'Don\'t stir too much while cooking to avoid breaking rice grains',
|
||||
'You can add cashews and raisins for extra richness'
|
||||
],
|
||||
nutritionInfo: {
|
||||
calories: 320,
|
||||
protein: '8g',
|
||||
carbs: '58g',
|
||||
fat: '6g'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'rajma-chawal',
|
||||
name: 'Rajma Chawal',
|
||||
description: 'Classic North Indian comfort food - kidney beans curry served with steamed basmati rice.',
|
||||
image: 'https://4m5m4tx28rtva30c.public.blob.vercel-storage.com/media/2025-09-07/rajma-chawal',
|
||||
cookTime: '45 mins',
|
||||
servings: 4,
|
||||
difficulty: 'Medium',
|
||||
category: 'Main Course',
|
||||
ingredients: [
|
||||
'2 cups Basmati rice',
|
||||
'1 cup dried kidney beans (rajma), soaked overnight',
|
||||
'2 large onions, finely chopped',
|
||||
'3 tomatoes, pureed',
|
||||
'1 tbsp ginger-garlic paste',
|
||||
'2 green chilies, chopped',
|
||||
'1 tsp cumin seeds',
|
||||
'1 tsp coriander powder',
|
||||
'1/2 tsp turmeric powder',
|
||||
'1 tsp red chili powder',
|
||||
'1 tsp garam masala',
|
||||
'3 tbsp oil or ghee',
|
||||
'Salt to taste',
|
||||
'Fresh coriander for garnish',
|
||||
'1 bay leaf'
|
||||
],
|
||||
instructions: [
|
||||
'Pressure cook soaked rajma with salt and bay leaf for 4-5 whistles until tender.',
|
||||
'Cook basmati rice separately with whole spices until fluffy. Keep warm.',
|
||||
'Heat oil in a pan. Add cumin seeds and let them splutter.',
|
||||
'Add chopped onions and cook until golden brown.',
|
||||
'Add ginger-garlic paste and green chilies. Cook for 2 minutes.',
|
||||
'Add tomato puree and cook until oil separates.',
|
||||
'Add all dry spices and cook for 1 minute.',
|
||||
'Add cooked rajma with its liquid. Simmer for 15-20 minutes.',
|
||||
'Mash some beans to thicken the gravy. Adjust consistency with water.',
|
||||
'Garnish with fresh coriander and serve hot with rice.',
|
||||
'Accompany with pickle, papad, and onion rings.'
|
||||
],
|
||||
tips: [
|
||||
'Soak rajma overnight for better cooking',
|
||||
'Add a pinch of baking soda while pressure cooking for softer beans',
|
||||
'The gravy should be thick but not dry'
|
||||
],
|
||||
nutritionInfo: {
|
||||
calories: 380,
|
||||
protein: '14g',
|
||||
carbs: '65g',
|
||||
fat: '8g'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'rice-kheer',
|
||||
name: 'Rice Kheer',
|
||||
description: 'Creamy and aromatic rice pudding made with milk, sugar, and cardamom - a perfect Indian dessert.',
|
||||
image: 'https://4m5m4tx28rtva30c.public.blob.vercel-storage.com/media/2025-09-07/rice-kheer',
|
||||
cookTime: '40 mins',
|
||||
servings: 6,
|
||||
difficulty: 'Easy',
|
||||
category: 'Dessert',
|
||||
ingredients: [
|
||||
'1/2 cup Basmati rice',
|
||||
'1 liter full-fat milk',
|
||||
'1/2 cup sugar (adjust to taste)',
|
||||
'4-5 green cardamom pods, crushed',
|
||||
'10-12 almonds, chopped',
|
||||
'10-12 pistachios, chopped',
|
||||
'2 tbsp raisins',
|
||||
'1 tbsp ghee',
|
||||
'1/4 tsp cardamom powder',
|
||||
'A pinch of saffron soaked in 2 tbsp warm milk',
|
||||
'1/2 tsp rose water (optional)'
|
||||
],
|
||||
instructions: [
|
||||
'Wash and soak rice for 30 minutes. Drain and set aside.',
|
||||
'Heat ghee in a heavy-bottomed pan. Add soaked rice and roast for 2-3 minutes.',
|
||||
'Add milk and bring to a boil. Reduce heat and simmer.',
|
||||
'Cook stirring occasionally until rice is completely soft and milk reduces to half.',
|
||||
'Add sugar and crushed cardamom. Mix well and cook for 5 more minutes.',
|
||||
'Add half of the chopped nuts and raisins. Cook for 2 minutes.',
|
||||
'Add saffron milk and cardamom powder. Mix gently.',
|
||||
'Add rose water if using. Cook for another 2 minutes.',
|
||||
'Garnish with remaining nuts and serve warm or chilled.',
|
||||
'The kheer will thicken as it cools, so adjust consistency accordingly.'
|
||||
],
|
||||
tips: [
|
||||
'Use full-fat milk for creamy texture',
|
||||
'Stir occasionally to prevent sticking',
|
||||
'Can be stored in refrigerator for 2-3 days'
|
||||
],
|
||||
nutritionInfo: {
|
||||
calories: 280,
|
||||
protein: '8g',
|
||||
carbs: '45g',
|
||||
fat: '8g'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'poha',
|
||||
name: 'Poha',
|
||||
description: 'Light and nutritious flattened rice breakfast dish with onions, potatoes, and aromatic spices.',
|
||||
image: 'https://4m5m4tx28rtva30c.public.blob.vercel-storage.com/media/2025-09-07/poha',
|
||||
cookTime: '15 mins',
|
||||
servings: 3,
|
||||
difficulty: 'Easy',
|
||||
category: 'Breakfast',
|
||||
ingredients: [
|
||||
'2 cups thick poha (flattened rice)',
|
||||
'2 medium potatoes, diced small',
|
||||
'1 large onion, chopped',
|
||||
'2 green chilies, chopped',
|
||||
'1 tsp ginger, minced',
|
||||
'8-10 curry leaves',
|
||||
'1/2 tsp mustard seeds',
|
||||
'1/2 tsp cumin seeds',
|
||||
'1/4 tsp turmeric powder',
|
||||
'1/2 tsp red chili powder',
|
||||
'2 tbsp oil',
|
||||
'Salt to taste',
|
||||
'2 tbsp roasted peanuts',
|
||||
'Fresh coriander leaves',
|
||||
'Lemon juice from 1 lemon',
|
||||
'1 tbsp sugar (optional)'
|
||||
],
|
||||
instructions: [
|
||||
'Rinse poha in a colander under cold water until soft. Drain well and set aside.',
|
||||
'Heat oil in a large pan. Add mustard seeds and let them splutter.',
|
||||
'Add cumin seeds, curry leaves, and green chilies.',
|
||||
'Add diced potatoes and cook until golden and crispy.',
|
||||
'Add chopped onions and ginger. Cook until onions are translucent.',
|
||||
'Add turmeric and red chili powder. Mix well.',
|
||||
'Add the drained poha gently. Mix carefully without mashing.',
|
||||
'Add salt, sugar, and roasted peanuts. Mix gently.',
|
||||
'Cook for 2-3 minutes until heated through.',
|
||||
'Add lemon juice and fresh coriander. Mix and serve immediately.',
|
||||
'Garnish with more coriander and serve with hot tea.'
|
||||
],
|
||||
tips: [
|
||||
'Don\'t over-rinse poha or it will become mushy',
|
||||
'Add vegetables like carrots and beans for variation',
|
||||
'Serve immediately for best texture'
|
||||
],
|
||||
nutritionInfo: {
|
||||
calories: 220,
|
||||
protein: '4g',
|
||||
carbs: '35g',
|
||||
fat: '8g'
|
||||
}
|
||||
},
|
||||
// {
|
||||
// id: 'moong-khichdi',
|
||||
// name: 'Moong Khichdi',
|
||||
// description: 'Wholesome and comforting one-pot meal made with rice and yellow lentils, perfect for easy digestion.',
|
||||
// image: 'https://images.unsplash.com/photo-1606491956689-2ea866880c84?w=600&h=400&fit=crop&q=80&fm=webp',
|
||||
// cookTime: '25 mins',
|
||||
// servings: 4,
|
||||
// difficulty: 'Easy',
|
||||
// category: 'Main Course',
|
||||
// ingredients: [
|
||||
// '1 cup Basmati rice',
|
||||
// '1/2 cup yellow moong dal (split)',
|
||||
// '1 tbsp ghee',
|
||||
// '1 tsp cumin seeds',
|
||||
// '1 inch ginger, minced',
|
||||
// '2 green chilies, slit',
|
||||
// '1/4 tsp turmeric powder',
|
||||
// '1/4 tsp asafoetida (hing)',
|
||||
// 'Salt to taste',
|
||||
// '4-5 cups water',
|
||||
// '1 tbsp ghee for tempering',
|
||||
// 'Fresh coriander leaves',
|
||||
// 'Black pepper powder to taste'
|
||||
// ],
|
||||
// instructions: [
|
||||
// 'Wash rice and moong dal together until water runs clear. Soak for 15 minutes.',
|
||||
// 'Heat ghee in a pressure cooker. Add cumin seeds and let them splutter.',
|
||||
// 'Add ginger, green chilies, and asafoetida. Sauté for 30 seconds.',
|
||||
// 'Add turmeric powder and the soaked rice-dal mixture.',
|
||||
// 'Add salt and water. The consistency should be like thick soup.',
|
||||
// 'Pressure cook for 3 whistles. Let pressure release naturally.',
|
||||
// 'Mash lightly with the back of a spoon for desired consistency.',
|
||||
// 'Heat ghee in a small pan for tempering. Add cumin seeds.',
|
||||
// 'Pour the tempering over khichdi and mix gently.',
|
||||
// 'Garnish with coriander and black pepper.',
|
||||
// 'Serve hot with yogurt, pickle, or ghee.'
|
||||
// ],
|
||||
// tips: [
|
||||
// 'Adjust water quantity for desired consistency',
|
||||
// 'Can add vegetables like carrots and peas',
|
||||
// 'Perfect comfort food when feeling unwell'
|
||||
// ],
|
||||
// nutritionInfo: {
|
||||
// calories: 260,
|
||||
// protein: '12g',
|
||||
// carbs: '48g',
|
||||
// fat: '6g'
|
||||
// }
|
||||
// },
|
||||
{
|
||||
id: 'vegetable-biryani',
|
||||
name: 'Vegetable Biryani',
|
||||
description: 'Aromatic and flavorful layered rice dish with mixed vegetables, herbs, and traditional biryani spices.',
|
||||
image: 'https://4m5m4tx28rtva30c.public.blob.vercel-storage.com/media/2025-09-07/vegetable-biryani',
|
||||
cookTime: '60 mins',
|
||||
servings: 6,
|
||||
difficulty: 'Hard',
|
||||
category: 'Main Course',
|
||||
ingredients: [
|
||||
'3 cups Basmati rice',
|
||||
'2 cups mixed vegetables (cauliflower, carrots, beans, peas)',
|
||||
'2 large onions, thinly sliced',
|
||||
'1 cup yogurt, whisked',
|
||||
'1/2 cup mint leaves',
|
||||
'1/2 cup coriander leaves',
|
||||
'1 tbsp ginger-garlic paste',
|
||||
'4 green chilies, slit',
|
||||
'1 tsp red chili powder',
|
||||
'1/2 tsp turmeric powder',
|
||||
'1 tsp garam masala',
|
||||
'4 tbsp ghee + oil for frying',
|
||||
'Whole spices: 4 cardamom, 2 bay leaves, 1 cinnamon stick, 4 cloves',
|
||||
'1/4 cup cashews and raisins',
|
||||
'Saffron soaked in 1/4 cup warm milk',
|
||||
'Salt to taste'
|
||||
],
|
||||
instructions: [
|
||||
'Soak basmati rice for 30 minutes. Boil with whole spices and salt until 70% cooked.',
|
||||
'Deep fry sliced onions until golden brown. Reserve half for garnish.',
|
||||
'Marinate vegetables with yogurt, ginger-garlic paste, and spices for 30 minutes.',
|
||||
'Cook marinated vegetables until tender. Add fried onions and half the herbs.',
|
||||
'In a heavy-bottomed pot, layer the vegetable curry at the bottom.',
|
||||
'Layer the partially cooked rice over vegetables.',
|
||||
'Sprinkle remaining fried onions, herbs, cashews, raisins, and saffron milk.',
|
||||
'Dot with ghee and cover with aluminum foil, then the lid.',
|
||||
'Cook on high heat for 3-4 minutes, then reduce to lowest heat for 45 minutes.',
|
||||
'Let it rest for 10 minutes before opening.',
|
||||
'Gently mix and serve with raita, boiled eggs, and shorba.'
|
||||
],
|
||||
tips: [
|
||||
'Use aged basmati rice for best results',
|
||||
'Don\'t fully cook rice in the first step',
|
||||
'The dum cooking process is crucial for authentic flavor'
|
||||
],
|
||||
nutritionInfo: {
|
||||
calories: 420,
|
||||
protein: '10g',
|
||||
carbs: '72g',
|
||||
fat: '12g'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'south-indian-basmati-rice',
|
||||
name: 'South Indian Basmati Rice',
|
||||
description: 'Fragrant basmati rice tempered with curry leaves, mustard seeds, and South Indian spices.',
|
||||
image: 'https://4m5m4tx28rtva30c.public.blob.vercel-storage.com/media/2025-09-07/south-indian-basmati-rice',
|
||||
cookTime: '20 mins',
|
||||
servings: 4,
|
||||
difficulty: 'Easy',
|
||||
category: 'Main Course',
|
||||
ingredients: [
|
||||
'2 cups Basmati rice',
|
||||
'3 tbsp coconut oil or ghee',
|
||||
'1 tsp mustard seeds',
|
||||
'1 tsp cumin seeds',
|
||||
'2 dry red chilies',
|
||||
'15-20 curry leaves',
|
||||
'1 inch ginger, minced',
|
||||
'2 green chilies, slit',
|
||||
'1/4 tsp turmeric powder',
|
||||
'1/4 tsp asafoetida (hing)',
|
||||
'1/4 cup cashews',
|
||||
'2 tbsp grated coconut (optional)',
|
||||
'Salt to taste',
|
||||
'3.5 cups water',
|
||||
'Fresh coriander leaves'
|
||||
],
|
||||
instructions: [
|
||||
'Wash basmati rice until water runs clear. Soak for 15 minutes and drain.',
|
||||
'Heat coconut oil in a heavy-bottomed pot.',
|
||||
'Add mustard seeds and cumin seeds. Let them splutter.',
|
||||
'Add dry red chilies, curry leaves, and cashews. Fry until cashews are golden.',
|
||||
'Add ginger, green chilies, and asafoetida. Sauté for 30 seconds.',
|
||||
'Add turmeric powder and the soaked rice. Mix gently for 2 minutes.',
|
||||
'Add salt and hot water. Bring to a boil.',
|
||||
'Reduce heat to low, cover and cook for 15-18 minutes.',
|
||||
'Let it rest for 5 minutes. Fluff with a fork.',
|
||||
'Garnish with grated coconut and coriander leaves.',
|
||||
'Serve with sambar, rasam, or any South Indian curry.'
|
||||
],
|
||||
tips: [
|
||||
'Fresh curry leaves make a big difference in flavor',
|
||||
'Don\'t skip the tempering step for authentic taste',
|
||||
'Perfect accompaniment to South Indian gravies'
|
||||
],
|
||||
nutritionInfo: {
|
||||
calories: 290,
|
||||
protein: '6g',
|
||||
carbs: '52g',
|
||||
fat: '8g'
|
||||
}
|
||||
}
|
||||
]
|
||||
82
lib/settings.ts
Normal file
82
lib/settings.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export interface SystemSettings {
|
||||
siteName: string
|
||||
siteDescription: string
|
||||
supportEmail: string
|
||||
minimumPayout: number
|
||||
enableReferrals: boolean
|
||||
enableCommissions: boolean
|
||||
maintenanceMode: boolean
|
||||
allowRegistration: boolean
|
||||
}
|
||||
|
||||
let cachedSettings: SystemSettings | null = null
|
||||
let cacheExpiry: number = 0
|
||||
|
||||
export async function getSystemSettings(): Promise<SystemSettings> {
|
||||
// Return cached settings if still valid (cache for 5 minutes)
|
||||
if (cachedSettings && Date.now() < cacheExpiry) {
|
||||
return cachedSettings
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if we're in Edge Runtime (middleware environment)
|
||||
if (typeof window === 'undefined' && 'EdgeRuntime' in globalThis) {
|
||||
throw new Error('Cannot use Prisma in Edge Runtime')
|
||||
}
|
||||
|
||||
let settings = await prisma.systemSettings.findFirst()
|
||||
|
||||
if (!settings) {
|
||||
// Create default settings if none exist
|
||||
settings = await prisma.systemSettings.create({
|
||||
data: {
|
||||
siteName: 'Padmaaja Rasooi',
|
||||
siteDescription: 'Premium Rice Products & Quality Grains',
|
||||
supportEmail: 'support@padmaajarasooi.com',
|
||||
minimumPayout: 100,
|
||||
enableReferrals: true,
|
||||
enableCommissions: true,
|
||||
maintenanceMode: false,
|
||||
allowRegistration: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
cachedSettings = {
|
||||
siteName: settings.siteName,
|
||||
siteDescription: settings.siteDescription,
|
||||
supportEmail: settings.supportEmail,
|
||||
minimumPayout: settings.minimumPayout,
|
||||
enableReferrals: settings.enableReferrals,
|
||||
enableCommissions: settings.enableCommissions,
|
||||
maintenanceMode: settings.maintenanceMode,
|
||||
allowRegistration: settings.allowRegistration
|
||||
}
|
||||
|
||||
// Cache for 5 minutes
|
||||
cacheExpiry = Date.now() + 5 * 60 * 1000
|
||||
|
||||
return cachedSettings
|
||||
} catch (error) {
|
||||
console.error('Error getting system settings:', error)
|
||||
|
||||
// Return default settings if database fails
|
||||
return {
|
||||
siteName: 'Padmaaja Rasooi',
|
||||
siteDescription: 'Premium Rice Products & Quality Grains',
|
||||
supportEmail: 'support@padmaajarasooi.com',
|
||||
minimumPayout: 100,
|
||||
enableReferrals: true,
|
||||
enableCommissions: true,
|
||||
maintenanceMode: false,
|
||||
allowRegistration: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function clearSettingsCache() {
|
||||
cachedSettings = null
|
||||
cacheExpiry = 0
|
||||
}
|
||||
185
lib/structured-data.ts
Normal file
185
lib/structured-data.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { Product } from '@/types'
|
||||
import {
|
||||
Product as SchemaProduct,
|
||||
WithContext,
|
||||
Organization,
|
||||
ContactPoint,
|
||||
PostalAddress,
|
||||
BreadcrumbList,
|
||||
FAQPage,
|
||||
LocalBusiness,
|
||||
ItemList
|
||||
} from 'schema-dts'
|
||||
|
||||
export function generateProductJsonLd(product: Product, baseUrl: string = ''): WithContext<SchemaProduct> {
|
||||
const currentPrice = product.discount
|
||||
? product.price - (product.price * product.discount / 100)
|
||||
: product.price
|
||||
|
||||
const productJsonLd: WithContext<SchemaProduct> = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Product",
|
||||
"@id": `${baseUrl}/products/${product.slug || product.id}`,
|
||||
"name": product.name,
|
||||
"description": product.description || `Premium quality ${product.category.name} rice from Padmaaja Rasooi`,
|
||||
"image": product.images.map(img => `${baseUrl}${img}`),
|
||||
"brand": {
|
||||
"@type": "Brand",
|
||||
"name": product.brand || "Padmaaja Rasooi"
|
||||
},
|
||||
"category": product.category.name,
|
||||
"sku": product.sku || product.id,
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"url": `${baseUrl}/products/${product.id}`,
|
||||
"priceCurrency": "INR",
|
||||
"price": currentPrice.toFixed(2),
|
||||
"priceValidUntil": new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||
"availability": product.stock && product.stock > 0
|
||||
? "https://schema.org/InStock"
|
||||
: "https://schema.org/OutOfStock",
|
||||
"seller": {
|
||||
"@type": "Organization",
|
||||
"name": "Padmaaja Rasooi",
|
||||
"url": baseUrl
|
||||
},
|
||||
},
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": "4.8",
|
||||
"bestRating": "5",
|
||||
"worstRating": "1",
|
||||
"ratingCount": "125"
|
||||
}
|
||||
}
|
||||
|
||||
return productJsonLd
|
||||
}
|
||||
|
||||
export function generateProductListJsonLd(products: Product[], baseUrl: string = ''): WithContext<ItemList> {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ItemList",
|
||||
"name": "Padmaaja Rasooi Premium Rice Products",
|
||||
"description": "Complete collection of premium quality rice products",
|
||||
"numberOfItems": products.length,
|
||||
"itemListElement": products.map((product, index) => ({
|
||||
"@type": "ListItem",
|
||||
"position": index + 1,
|
||||
"item": {
|
||||
"@type": "Product",
|
||||
"@id": `${baseUrl}/products/${product.slug || product.id}`,
|
||||
"name": product.name,
|
||||
"image": product.images[0] ? `${baseUrl}${product.images[0]}` : '',
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"priceCurrency": "INR",
|
||||
"price": (product.discount
|
||||
? product.price - (product.price * product.discount / 100)
|
||||
: product.price).toFixed(2)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export function generateOrganizationJsonLd(baseUrl: string = ''): WithContext<Organization> {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "Padmaaja Rasooi",
|
||||
"url": baseUrl,
|
||||
"logo": `${baseUrl}/images/logo.png`,
|
||||
"description": "Premium rice products and quality grains offering the finest quality rice sourced directly from certified farms.",
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"telephone": "+91-9876543210",
|
||||
"email": "contact@padmaajarasooi.com",
|
||||
"contactType": "Customer Service"
|
||||
},
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "123 Rice Market Street",
|
||||
"addressLocality": "Hyderabad",
|
||||
"addressRegion": "Telangana",
|
||||
"postalCode": "500001",
|
||||
"addressCountry": "IN"
|
||||
},
|
||||
"sameAs": [
|
||||
"https://www.facebook.com/padmaajarasooi",
|
||||
"https://www.instagram.com/padmaajarasooi",
|
||||
"https://www.linkedin.com/company/padmaajarasooi"
|
||||
],
|
||||
"foundingDate": "2020"
|
||||
}
|
||||
}
|
||||
|
||||
export function generateBreadcrumbJsonLd(breadcrumbs: Array<{ name: string, url: string }>, baseUrl: string = ''): WithContext<BreadcrumbList> {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": breadcrumbs.map((crumb, index) => ({
|
||||
"@type": "ListItem",
|
||||
"position": index + 1,
|
||||
"name": crumb.name,
|
||||
"item": `${baseUrl}${crumb.url}`
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export function generateFAQJsonLd(faqs: Array<{ question: string, answer: string }>): WithContext<FAQPage> {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": faqs.map(faq => ({
|
||||
"@type": "Question",
|
||||
"name": faq.question,
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": faq.answer
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export function generateLocalBusinessJsonLd(baseUrl: string = ''): WithContext<LocalBusiness> {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "LocalBusiness",
|
||||
"name": "Padmaaja Rasooi",
|
||||
"image": `${baseUrl}/images/business-photo.jpg`,
|
||||
"description": "Premium rice products and quality grains",
|
||||
"url": baseUrl,
|
||||
"telephone": "+91-9876543210",
|
||||
"email": "contact@padmaajarasooi.com",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "123 Rice Market Street",
|
||||
"addressLocality": "Hyderabad",
|
||||
"addressRegion": "Telangana",
|
||||
"postalCode": "500001",
|
||||
"addressCountry": "IN"
|
||||
},
|
||||
"geo": {
|
||||
"@type": "GeoCoordinates",
|
||||
"latitude": "17.3850",
|
||||
"longitude": "78.4867"
|
||||
},
|
||||
"openingHoursSpecification": [
|
||||
{
|
||||
"@type": "OpeningHoursSpecification",
|
||||
"dayOfWeek": [
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday"
|
||||
],
|
||||
"opens": "09:00",
|
||||
"closes": "18:00"
|
||||
}
|
||||
],
|
||||
"priceRange": "₹₹"
|
||||
}
|
||||
}
|
||||
37
lib/utils.ts
Normal file
37
lib/utils.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatCurrency(amount: number) {
|
||||
return new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'INR',
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
export function formatDate(date: string | Date) {
|
||||
return new Date(date).toLocaleDateString('en-IN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
export function generateSlug(text: string) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w ]+/g, '')
|
||||
.replace(/ +/g, '-');
|
||||
}
|
||||
|
||||
export function generateSKU() {
|
||||
return (
|
||||
'SKU-' +
|
||||
Date.now() +
|
||||
'-' +
|
||||
Math.random().toString(36).substr(2, 9).toUpperCase()
|
||||
);
|
||||
}
|
||||
22
lib/validations/auth.ts
Normal file
22
lib/validations/auth.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const signInSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||
})
|
||||
|
||||
export const signUpSchema = z.object({
|
||||
name: z.string().min(2, 'Name must be at least 2 characters'),
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||
confirmPassword: z.string(),
|
||||
phone: z.string().optional(),
|
||||
referralCode: z.string().optional(),
|
||||
role: z.enum(['CUSTOMER', 'MEMBER']).optional(),
|
||||
}).refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"],
|
||||
})
|
||||
|
||||
export type SignInValues = z.infer<typeof signInSchema>
|
||||
export type SignUpValues = z.infer<typeof signUpSchema>
|
||||
23
lib/validations/product.ts
Normal file
23
lib/validations/product.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const productSchema = z.object({
|
||||
name: z.string().min(1, 'Product name is required'),
|
||||
description: z.string().optional(),
|
||||
price: z.number().positive('Price must be positive'),
|
||||
discount: z.number().min(0).max(100).default(0),
|
||||
images: z.array(z.string().url()).default([]),
|
||||
stock: z.number().int().min(0).default(0),
|
||||
categoryId: z.string().min(1, 'Category is required'),
|
||||
sku: z.string().min(1, 'SKU is required'),
|
||||
slug: z.string().min(1, 'Slug is required')
|
||||
})
|
||||
|
||||
export const categorySchema = z.object({
|
||||
name: z.string().min(1, 'Category name is required'),
|
||||
description: z.string().optional(),
|
||||
image: z.string().url().optional(),
|
||||
isActive: z.boolean().default(true)
|
||||
})
|
||||
|
||||
export type ProductInput = z.infer<typeof productSchema>
|
||||
export type CategoryInput = z.infer<typeof categorySchema>
|
||||
109
lib/version-checker.ts
Normal file
109
lib/version-checker.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface VersionInfo {
|
||||
version: string
|
||||
buildTime: string
|
||||
environment: string
|
||||
}
|
||||
|
||||
export function useVersionChecker() {
|
||||
const [currentVersion, setCurrentVersion] = useState<VersionInfo | null>(null)
|
||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Get current version from meta tag or build info
|
||||
const getCurrentVersion = (): VersionInfo => {
|
||||
return {
|
||||
version: process.env.NEXT_PUBLIC_APP_VERSION || '1.0.0',
|
||||
buildTime: process.env.NEXT_PUBLIC_BUILD_TIME || new Date().toISOString(),
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
}
|
||||
}
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
try {
|
||||
// Fetch version info from a version endpoint
|
||||
const response = await fetch('/api/version', {
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const latestVersion: VersionInfo = await response.json()
|
||||
const current = getCurrentVersion()
|
||||
|
||||
// Compare versions
|
||||
if (latestVersion.version !== current.version ||
|
||||
latestVersion.buildTime !== current.buildTime) {
|
||||
setIsUpdateAvailable(true)
|
||||
setCurrentVersion(latestVersion)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Version check failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for updates on mount
|
||||
checkForUpdates()
|
||||
|
||||
// Set up periodic checks (every 5 minutes)
|
||||
const interval = setInterval(checkForUpdates, 5 * 60 * 1000)
|
||||
|
||||
// Listen for service worker updates
|
||||
const handleServiceWorkerMessage = (event: MessageEvent) => {
|
||||
if (event.data && event.data.type === 'SW_UPDATED') {
|
||||
setIsUpdateAvailable(true)
|
||||
setCurrentVersion({
|
||||
version: event.data.version,
|
||||
buildTime: event.data.timestamp,
|
||||
environment: process.env.NODE_ENV || 'production'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
navigator.serviceWorker?.addEventListener('message', handleServiceWorkerMessage)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
navigator.serviceWorker?.removeEventListener('message', handleServiceWorkerMessage)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const refreshApp = () => {
|
||||
// Clear all caches
|
||||
if ('caches' in window) {
|
||||
caches.keys().then(names => {
|
||||
names.forEach(name => {
|
||||
caches.delete(name)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Unregister service workers
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
registrations.forEach(registration => {
|
||||
registration.unregister()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Clear local storage (optional - be careful with user data)
|
||||
// localStorage.clear()
|
||||
|
||||
// Hard refresh
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
return {
|
||||
currentVersion,
|
||||
isUpdateAvailable,
|
||||
refreshApp
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user