first commit

This commit is contained in:
2026-01-17 14:17:42 +05:30
commit 0f194eb9e7
328 changed files with 73544 additions and 0 deletions

268
lib/blob-storage.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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`
})
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>

View 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
View 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
}
}