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

31
.env.example Normal file
View File

@@ -0,0 +1,31 @@
# Database
DATABASE_URL="postgresql://username:password@localhost:5432/padmaaja_rasooi_db"
# NextAuth
NEXTAUTH_SECRET="your-nextauth-secret"
NEXTAUTH_URL="http://localhost:3000"
# Google OAuth
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
# Razorpay - Test Environment
RAZORPAY_TEST_KEY_ID="rzp_test_your_test_key_id"
RAZORPAY_TEST_KEY_SECRET="your_test_key_secret"
# Razorpay - Production Environment
RAZORPAY_LIVE_KEY_ID="rzp_live_your_live_key_id"
RAZORPAY_LIVE_KEY_SECRET="your_live_key_secret"
# Environment mode (test or production)
RAZORPAY_ENV="test"
# Vercel Blob Storage
BLOB_READ_WRITE_TOKEN="vercel_blob_rw_your_token_here"
# SMTP Configuration (for email notifications)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD="your-app-password"
SMTP_FROM=your-email@gmail.com

7
.eslintrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"extends": ["next/core-web-vitals"],
"rules": {
"react/no-unescaped-entities": "off",
"@next/next/no-page-custom-font": "off"
}
}

38
.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
/lib/generated/prisma

1
.node-version Normal file
View File

@@ -0,0 +1 @@
20.19.0

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
legacy-peer-deps=true
auto-install-peers=true
strict-peer-deps=false

15
.vercelignore Normal file
View File

@@ -0,0 +1,15 @@
/node_modules
/.next
/coverage
/.env.local
/.env.development.local
/.env.test.local
/.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
.vscode
*.log
.env
prisma/migrations/

View File

@@ -0,0 +1,326 @@
import { Metadata } from 'next'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { Shield, Eye, Lock, Users, Mail, Phone } from 'lucide-react'
export const metadata: Metadata = {
title: 'Privacy Policy | Padmaaja Rasooi - Your Data Protection Rights',
description: 'Learn how Padmaaja Rasooi protects your personal information, handles data collection, and ensures your privacy rights are respected.',
keywords: 'privacy policy, data protection, personal information, GDPR compliance, data security, Padmaaja Rasooi',
openGraph: {
title: 'Privacy Policy | Padmaaja Rasooi',
description: 'Comprehensive privacy policy outlining how we protect your personal data and respect your privacy rights.',
type: 'website',
},
}
export default function PrivacyPolicyPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 via-white to-amber-50">
<div className="container mx-auto px-4 py-12 max-w-4xl">
{/* Header Section */}
<div className="text-center mb-12">
<div className="flex items-center justify-center mb-6">
<Shield className="h-12 w-12 text-green-600 mr-4" />
<h1 className="text-4xl font-bold text-gray-900">Privacy Policy</h1>
</div>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
At Padmaaja Rasooi, we are committed to protecting your privacy and ensuring
the security of your personal information. This policy explains how we collect,
use, and safeguard your data.
</p>
<Badge variant="outline" className="mt-4">
Last Updated: August 30, 2025
</Badge>
</div>
{/* Introduction */}
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center">
<Eye className="h-5 w-5 mr-2 text-green-600" />
Our Commitment to Privacy
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-700 leading-relaxed">
Padmaaja Rasooi (&quot;we,&quot; &quot;our,&quot; or &quot;us&quot;) respects your privacy and is committed to protecting
your personal data. This privacy policy will inform you about how we look after your
personal data when you visit our website, purchase our premium rice products, or engage
with our business partnership program.
</p>
<p className="text-gray-700 leading-relaxed">
This policy applies to all visitors, customers, and participants in our business network.
We are committed to complying with applicable data protection laws, including GDPR and
local privacy regulations.
</p>
</CardContent>
</Card>
{/* Information We Collect */}
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center">
<Users className="h-5 w-5 mr-2 text-green-600" />
Information We Collect
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Personal Information</h3>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li>Name, email address, phone number, and postal address</li>
<li>Date of birth and government-issued ID (for age verification)</li>
<li>Bank account details and payment information (securely processed)</li>
<li>Profile photos and identification documents</li>
<li>Communication preferences and marketing consents</li>
</ul>
</div>
<Separator />
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Business Information</h3>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li>Business network position and referral relationships</li>
<li>Sales volume, commissions, and earnings data</li>
<li>Order history and product preferences</li>
<li>Performance metrics and achievement records</li>
<li>Training completion and certification status</li>
</ul>
</div>
<Separator />
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Technical Information</h3>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li>IP address, browser type, and device information</li>
<li>Website usage data and navigation patterns</li>
<li>Cookie data and local storage information</li>
<li>Session recordings for customer support purposes</li>
<li>Location data (with your consent)</li>
</ul>
</div>
</CardContent>
</Card>
{/* How We Use Information */}
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center">
<Lock className="h-5 w-5 mr-2 text-green-600" />
How We Use Your Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-3">
<h3 className="font-semibold text-gray-900">Service Provision</h3>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Process orders and deliver products</li>
<li>Manage your business account and network</li>
<li>Calculate and process commissions</li>
<li>Provide customer support services</li>
</ul>
</div>
<div className="space-y-3">
<h3 className="font-semibold text-gray-900">Communication</h3>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Send order confirmations and updates</li>
<li>Provide business opportunity information</li>
<li>Share promotional offers (with consent)</li>
<li>Send important policy updates</li>
</ul>
</div>
<div className="space-y-3">
<h3 className="font-semibold text-gray-900">Legal Compliance</h3>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Verify identity and prevent fraud</li>
<li>Comply with tax reporting requirements</li>
<li>Meet regulatory obligations</li>
<li>Respond to legal requests</li>
</ul>
</div>
<div className="space-y-3">
<h3 className="font-semibold text-gray-900">Business Improvement</h3>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Analyze website usage and performance</li>
<li>Improve products and services</li>
<li>Develop new business features</li>
<li>Conduct market research</li>
</ul>
</div>
</div>
</CardContent>
</Card>
{/* Data Sharing */}
<Card className="mb-8">
<CardHeader>
<CardTitle>Data Sharing and Disclosure</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-700 leading-relaxed">
We do not sell, trade, or rent your personal information to third parties.
We may share your information only in the following circumstances:
</p>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<h4 className="font-semibold text-amber-800 mb-2">Authorized Sharing</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-amber-700">
<li>With your upline/downline for legitimate business purposes</li>
<li>With payment processors for transaction processing</li>
<li>With logistics partners for product delivery</li>
<li>With government authorities when legally required</li>
<li>With professional advisors under confidentiality agreements</li>
</ul>
</div>
</CardContent>
</Card>
{/* Data Security */}
<Card className="mb-8">
<CardHeader>
<CardTitle>Data Security Measures</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-700 leading-relaxed">
We implement appropriate technical and organizational measures to protect
your personal data against unauthorized access, alteration, disclosure, or destruction.
</p>
<div className="grid md:grid-cols-3 gap-4">
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<h4 className="font-semibold text-green-800 mb-2">Technical Safeguards</h4>
<ul className="list-disc list-inside space-y-1 text-xs text-green-700">
<li>SSL/TLS encryption</li>
<li>Secure data centers</li>
<li>Regular security audits</li>
<li>Access controls</li>
</ul>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-semibold text-blue-800 mb-2">Administrative Controls</h4>
<ul className="list-disc list-inside space-y-1 text-xs text-blue-700">
<li>Employee training</li>
<li>Data handling policies</li>
<li>Incident response procedures</li>
<li>Regular updates</li>
</ul>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<h4 className="font-semibold text-purple-800 mb-2">Physical Security</h4>
<ul className="list-disc list-inside space-y-1 text-xs text-purple-700">
<li>Secured facilities</li>
<li>Access restrictions</li>
<li>Surveillance systems</li>
<li>Device protection</li>
</ul>
</div>
</div>
</CardContent>
</Card>
{/* Your Rights */}
<Card className="mb-8">
<CardHeader>
<CardTitle>Your Privacy Rights</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-700 leading-relaxed">
You have several rights regarding your personal data. You may exercise these
rights by contacting us using the information provided below.
</p>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<h4 className="font-semibold text-gray-900">Access & Portability</h4>
<p className="text-sm text-gray-600">
Request access to your personal data and receive a copy in a portable format.
</p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-gray-900">Rectification</h4>
<p className="text-sm text-gray-600">
Request correction of inaccurate or incomplete personal data.
</p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-gray-900">Erasure</h4>
<p className="text-sm text-gray-600">
Request deletion of your personal data (subject to legal obligations).
</p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-gray-900">Restriction</h4>
<p className="text-sm text-gray-600">
Request limitation of processing of your personal data.
</p>
</div>
</div>
</CardContent>
</Card>
{/* Contact Information */}
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center">
<Mail className="h-5 w-5 mr-2 text-green-600" />
Contact Us
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-700 mb-4">
If you have any questions about this privacy policy or wish to exercise your
privacy rights, please contact us:
</p>
<div className="bg-gray-50 border rounded-lg p-4 space-y-2">
<div className="flex items-center">
<Mail className="h-4 w-4 text-gray-600 mr-2" />
<span className="text-sm">Email: privacy@padmaajarasooi.com</span>
</div>
<div className="flex items-center">
<Phone className="h-4 w-4 text-gray-600 mr-2" />
<span className="text-sm">Phone: +91 (Customer Service)</span>
</div>
<div className="flex items-start">
<div className="h-4 w-4 text-gray-600 mr-2 mt-0.5">📍</div>
<span className="text-sm">
Address: Padmaaja Rasooi Headquarters<br />
[Complete Address]<br />
India
</span>
</div>
</div>
</CardContent>
</Card>
{/* Updates Notice */}
<Card>
<CardContent className="pt-6">
<div className="text-center">
<p className="text-sm text-gray-600 mb-2">
We may update this privacy policy from time to time. We will notify you of
any material changes by posting the new policy on our website.
</p>
<Badge variant="secondary">
Effective Date: August 30, 2025
</Badge>
</div>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,486 @@
import { Metadata } from 'next'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { RotateCcw, Clock, CheckCircle, XCircle, AlertCircle, Mail, Phone } from 'lucide-react'
export const metadata: Metadata = {
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',
openGraph: {
title: 'Refund Policy | Padmaaja Rasooi',
description: 'Clear guidelines for product returns and refunds to ensure customer satisfaction.',
type: 'website',
},
}
export default function RefundPolicyPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-pink-50">
<div className="container mx-auto px-4 py-12 max-w-4xl">
{/* Header Section */}
<div className="text-center mb-12">
<div className="flex items-center justify-center mb-6">
<RotateCcw className="h-12 w-12 text-purple-600 mr-4" />
<h1 className="text-4xl font-bold text-gray-900">Refund Policy</h1>
</div>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
We stand behind the quality of our premium rice products. This policy outlines
our commitment to customer satisfaction and the process for returns and refunds.
</p>
<Badge variant="outline" className="mt-4">
Last Updated: August 30, 2025
</Badge>
</div>
{/* Satisfaction Guarantee */}
<Alert className="mb-8 border-green-200 bg-green-50">
<CheckCircle className="h-4 w-4 text-green-600" />
<AlertDescription className="text-green-800">
<strong>100% Satisfaction Guarantee:</strong> We are committed to your satisfaction.
If you&apos;re not completely happy with your purchase, we&apos;ll make it right within our policy guidelines.
</AlertDescription>
</Alert>
{/* Return Eligibility */}
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center">
<CheckCircle className="h-5 w-5 mr-2 text-purple-600" />
Return Eligibility
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">Eligible Returns</h3>
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-3">
<h4 className="font-medium text-green-800"> Returnable Items</h4>
<ul className="list-disc list-inside space-y-2 text-sm text-gray-700">
<li>Unopened rice packages in original packaging</li>
<li>Products damaged during shipping</li>
<li>Wrong items delivered due to our error</li>
<li>Defective or contaminated products</li>
<li>Products not matching the description</li>
</ul>
</div>
<div className="space-y-3">
<h4 className="font-medium text-red-800"> Non-Returnable Items</h4>
<ul className="list-disc list-inside space-y-2 text-sm text-gray-700">
<li>Opened or used rice products (food safety)</li>
<li>Products damaged by customer mishandling</li>
<li>Items purchased on clearance or special offer</li>
<li>Products past 30-day return window</li>
<li>Customized or personalized products</li>
</ul>
</div>
</div>
</div>
<Separator />
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">Return Conditions</h3>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<ul className="space-y-2 text-sm text-blue-800">
<li><strong>Time Limit:</strong> Returns must be initiated within 30 days of delivery</li>
<li><strong>Original Packaging:</strong> Items must be in original, unopened packaging</li>
<li><strong>Proof of Purchase:</strong> Valid order number or receipt required</li>
<li><strong>Product Condition:</strong> Items must be unused and in resalable condition</li>
<li><strong>Return Authorization:</strong> Pre-approval required before sending items back</li>
</ul>
</div>
</div>
</CardContent>
</Card>
{/* Return Process */}
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center">
<Clock className="h-5 w-5 mr-2 text-purple-600" />
Return Process
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid md:grid-cols-4 gap-4">
<div className="text-center p-4 border border-purple-200 rounded-lg bg-purple-50">
<div className="text-2xl font-bold text-purple-600 mb-2">1</div>
<div className="text-sm font-medium text-purple-800">Contact Us</div>
<div className="text-xs text-purple-700 mt-1">
Reach out within 30 days of delivery
</div>
</div>
<div className="text-center p-4 border border-blue-200 rounded-lg bg-blue-50">
<div className="text-2xl font-bold text-blue-600 mb-2">2</div>
<div className="text-sm font-medium text-blue-800">Get Authorization</div>
<div className="text-xs text-blue-700 mt-1">
Receive return authorization number
</div>
</div>
<div className="text-center p-4 border border-green-200 rounded-lg bg-green-50">
<div className="text-2xl font-bold text-green-600 mb-2">3</div>
<div className="text-sm font-medium text-green-800">Ship Items</div>
<div className="text-xs text-green-700 mt-1">
Package and send items back to us
</div>
</div>
<div className="text-center p-4 border border-amber-200 rounded-lg bg-amber-50">
<div className="text-2xl font-bold text-amber-600 mb-2">4</div>
<div className="text-sm font-medium text-amber-800">Get Refund</div>
<div className="text-xs text-amber-700 mt-1">
Receive refund after inspection
</div>
</div>
</div>
<Separator />
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">Detailed Steps</h3>
<div className="space-y-4">
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
<span className="text-sm font-semibold text-purple-600">1</span>
</div>
<div>
<h4 className="font-medium text-gray-900">Initiate Return Request</h4>
<p className="text-sm text-gray-700">
Contact our customer service team via email or phone with your order number,
reason for return, and photos if applicable (for damaged items).
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<span className="text-sm font-semibold text-blue-600">2</span>
</div>
<div>
<h4 className="font-medium text-gray-900">Return Authorization</h4>
<p className="text-sm text-gray-700">
Once approved, you&apos;ll receive a Return Authorization (RA) number and
return shipping instructions. Do not ship without an RA number.
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<span className="text-sm font-semibold text-green-600">3</span>
</div>
<div>
<h4 className="font-medium text-gray-900">Package & Ship</h4>
<p className="text-sm text-gray-700">
Securely package the items in original packaging, include the RA number,
and ship to the provided return address. Keep tracking information.
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-8 h-8 bg-amber-100 rounded-full flex items-center justify-center">
<span className="text-sm font-semibold text-amber-600">4</span>
</div>
<div>
<h4 className="font-medium text-gray-900">Inspection & Refund</h4>
<p className="text-sm text-gray-700">
We&apos;ll inspect the returned items within 3-5 business days and process
your refund if everything meets our return conditions.
</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Refund Methods & Timelines */}
<Card className="mb-8">
<CardHeader>
<CardTitle>Refund Methods & Timelines</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">Refund Options</h3>
<div className="grid md:grid-cols-3 gap-4">
<div className="border rounded-lg p-4 hover:shadow-md transition-shadow">
<div className="text-center">
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
<span className="text-green-600 font-bold"></span>
</div>
<h4 className="font-semibold text-gray-900 mb-2">Original Payment Method</h4>
<p className="text-sm text-gray-700">
Refund to the original payment method used for purchase
</p>
<div className="mt-3 text-xs text-green-600 font-medium">
5-10 business days
</div>
</div>
</div>
<div className="border rounded-lg p-4 hover:shadow-md transition-shadow">
<div className="text-center">
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-3">
<span className="text-blue-600 font-bold">🏦</span>
</div>
<h4 className="font-semibold text-gray-900 mb-2">Bank Transfer</h4>
<p className="text-sm text-gray-700">
Direct transfer to your bank account (for large orders)
</p>
<div className="mt-3 text-xs text-blue-600 font-medium">
3-5 business days
</div>
</div>
</div>
<div className="border rounded-lg p-4 hover:shadow-md transition-shadow">
<div className="text-center">
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-3">
<span className="text-purple-600 font-bold">💳</span>
</div>
<h4 className="font-semibold text-gray-900 mb-2">Store Credit</h4>
<p className="text-sm text-gray-700">
Account credit for future purchases (optional)
</p>
<div className="mt-3 text-xs text-purple-600 font-medium">
Immediate
</div>
</div>
</div>
</div>
</div>
<Separator />
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">Processing Timeline</h3>
<div className="bg-gray-50 border rounded-lg p-4">
<div className="grid md:grid-cols-4 gap-4 text-center">
<div>
<div className="text-lg font-bold text-gray-900">1-2 Days</div>
<div className="text-sm text-gray-600">Return Received</div>
</div>
<div>
<div className="text-lg font-bold text-gray-900">3-5 Days</div>
<div className="text-sm text-gray-600">Quality Inspection</div>
</div>
<div>
<div className="text-lg font-bold text-gray-900">1 Day</div>
<div className="text-sm text-gray-600">Refund Processing</div>
</div>
<div>
<div className="text-lg font-bold text-gray-900">5-10 Days</div>
<div className="text-sm text-gray-600">Amount Credited</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Shipping Costs */}
<Card className="mb-8">
<CardHeader>
<CardTitle>Return Shipping Policy</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-3">
<h4 className="font-semibold text-green-800">🆓 Free Return Shipping</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Defective or damaged products</li>
<li>Wrong items sent due to our error</li>
<li>Products not matching description</li>
<li>Quality issues with rice</li>
</ul>
</div>
<div className="space-y-3">
<h4 className="font-semibold text-amber-800">💰 Customer Pays Shipping</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Change of mind returns</li>
<li>Ordered wrong quantity/variety</li>
<li>No longer needed</li>
<li>Personal preference changes</li>
</ul>
</div>
</div>
<Alert className="border-blue-200 bg-blue-50">
<AlertCircle className="h-4 w-4 text-blue-600" />
<AlertDescription className="text-blue-800">
<strong>Tip:</strong> For expensive return shipping, consider our exchange option
instead of a refund to save on shipping costs.
</AlertDescription>
</Alert>
</CardContent>
</Card>
{/* Exchanges */}
<Card className="mb-8">
<CardHeader>
<CardTitle>Exchange Policy</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-700 leading-relaxed">
We offer exchanges for different rice varieties or quantities of equal or lesser value.
Exchanges follow the same timeline as returns but may be faster since no refund processing is required.
</p>
<div className="grid md:grid-cols-2 gap-4">
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<h4 className="font-semibold text-green-800 mb-2">Exchange Benefits</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-green-700">
<li>Faster processing than refunds</li>
<li>Try different rice varieties</li>
<li>Adjust order quantities</li>
<li>No refund processing delays</li>
</ul>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-semibold text-blue-800 mb-2">Exchange Process</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-blue-700">
<li>Follow same return authorization process</li>
<li>Specify desired exchange item</li>
<li>Pay any price difference if applicable</li>
<li>Receive new shipment after inspection</li>
</ul>
</div>
</div>
</CardContent>
</Card>
{/* Special Circumstances */}
<Card className="mb-8">
<CardHeader>
<CardTitle>Special Circumstances</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">Business Partnership Returns</h3>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<p className="text-amber-800 text-sm mb-3">
<strong>Important for Business Partners:</strong> Special rules apply for business inventory returns.
</p>
<ul className="list-disc list-inside space-y-1 text-sm text-amber-700">
<li>Wholesale purchases may have different return terms</li>
<li>Commission adjustments may apply to returned inventory</li>
<li>Bulk order returns require additional approval</li>
<li>Seasonal promotion items may have restricted return periods</li>
</ul>
</div>
</div>
<Separator />
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">Holiday Season Policy</h3>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<p className="text-purple-800 text-sm mb-3">
<strong>Extended Holiday Returns:</strong> Special provisions during festival seasons.
</p>
<ul className="list-disc list-inside space-y-1 text-sm text-purple-700">
<li>Extended 45-day return window during Diwali season</li>
<li>Gift purchases have extended return periods</li>
<li>Festival bulk orders eligible for partial returns</li>
<li>Special handling for wedding and celebration orders</li>
</ul>
</div>
</div>
</CardContent>
</Card>
{/* Contact for Returns */}
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center">
<Mail className="h-5 w-5 mr-2 text-purple-600" />
Contact for Returns
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-700 leading-relaxed">
Our customer service team is here to help with all return and refund requests.
Please have your order number ready when contacting us.
</p>
<div className="grid md:grid-cols-2 gap-6">
<div className="bg-gray-50 border rounded-lg p-4 space-y-3">
<h4 className="font-semibold text-gray-900">Customer Service</h4>
<div className="space-y-2">
<div className="flex items-center">
<Mail className="h-4 w-4 text-gray-600 mr-2" />
<span className="text-sm">returns@padmaajarasooi.com</span>
</div>
<div className="flex items-center">
<Phone className="h-4 w-4 text-gray-600 mr-2" />
<span className="text-sm">+91 [Customer Service]</span>
</div>
<div className="text-sm text-gray-600">
<strong>Hours:</strong> Mon-Sat, 9:00 AM - 6:00 PM IST
</div>
</div>
</div>
<div className="bg-gray-50 border rounded-lg p-4 space-y-3">
<h4 className="font-semibold text-gray-900">Return Address</h4>
<div className="text-sm text-gray-700">
<strong>Padmaaja Rasooi Returns Center</strong><br />
[Return Processing Facility Address]<br />
[City, State, PIN Code]<br />
India
</div>
<div className="text-xs text-red-600">
Do not ship without Return Authorization number
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-3 mt-6">
<Button variant="default" className="flex-1">
<Mail className="h-4 w-4 mr-2" />
Start Return Request
</Button>
<Button variant="outline" className="flex-1">
<Phone className="h-4 w-4 mr-2" />
Call Customer Service
</Button>
</div>
</CardContent>
</Card>
{/* Final Notes */}
<Card>
<CardContent className="pt-6">
<div className="text-center space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Important Notes</h3>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<ul className="text-sm text-yellow-800 space-y-2">
<li> This policy applies to purchases made directly from Padmaaja Rasooi</li>
<li> Third-party retailer purchases may have different return policies</li>
<li> We reserve the right to refuse returns that don&apos;t meet our conditions</li>
<li> Fraudulent return attempts will result in account suspension</li>
<li> This policy is subject to change with 30 days notice</li>
</ul>
</div>
<Badge variant="secondary" className="mt-4">
Policy Effective: August 30, 2025
</Badge>
</div>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,443 @@
import { Metadata } from 'next'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { FileText, Scale, AlertTriangle, Users, ShoppingCart, Gavel } from 'lucide-react'
export const metadata: Metadata = {
title: 'Terms of Service | Padmaaja Rasooi - Business Agreement & Policies',
description: 'Read our comprehensive terms of service covering business partnership, product purchases, and business conduct for Padmaaja Rasooi premium rice products.',
keywords: 'terms of service, business partnership, business agreement, legal terms, conditions, Padmaaja Rasooi, rice products',
openGraph: {
title: 'Terms of Service | Padmaaja Rasooi',
description: 'Legal terms and conditions for participating in our business network and purchasing our premium rice products.',
type: 'website',
},
}
export default function TermsOfServicePage() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-green-50">
<div className="container mx-auto px-4 py-12 max-w-4xl">
{/* Header Section */}
<div className="text-center mb-12">
<div className="flex items-center justify-center mb-6">
<Scale className="h-12 w-12 text-blue-600 mr-4" />
<h1 className="text-4xl font-bold text-gray-900">Terms of Service</h1>
</div>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
These terms govern your use of our website, purchase of products, and participation
in the Padmaaja Rasooi business partnership program.
</p>
<Badge variant="outline" className="mt-4">
Last Updated: August 30, 2025
</Badge>
</div>
{/* Important Notice */}
<Alert className="mb-8 border-amber-200 bg-amber-50">
<AlertTriangle className="h-4 w-4 text-amber-600" />
<AlertDescription className="text-amber-800">
<strong>Important:</strong> By accessing our website, purchasing our products, or participating
in our business partnership program, you agree to be bound by these terms. Please read them carefully.
</AlertDescription>
</Alert>
{/* Acceptance of Terms */}
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center">
<FileText className="h-5 w-5 mr-2 text-blue-600" />
Acceptance of Terms
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-700 leading-relaxed">
These Terms of Service (&quot;Terms&quot;) constitute a legally binding agreement between you
and Padmaaja Rasooi (&quot;Company,&quot; &quot;we,&quot; &quot;our,&quot; or &quot;us&quot;). These Terms apply to:
</p>
<div className="grid md:grid-cols-3 gap-4">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-semibold text-blue-800 mb-2">Website Usage</h4>
<p className="text-sm text-blue-700">
Browsing, accessing, and using our website and mobile applications.
</p>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<h4 className="font-semibold text-green-800 mb-2">Product Purchases</h4>
<p className="text-sm text-green-700">
Buying our premium rice products for personal consumption or resale.
</p>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<h4 className="font-semibold text-purple-800 mb-2">Business Partnership</h4>
<p className="text-sm text-purple-700">
Joining and participating in our rice business partnership opportunity.
</p>
</div>
</div>
</CardContent>
</Card>
{/* Business Partnership Terms */}
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center">
<Users className="h-5 w-5 mr-2 text-blue-600" />
Business Partnership Program
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Eligibility Requirements</h3>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li>Must be at least 18 years of age or the age of majority in your jurisdiction</li>
<li>Must provide accurate and complete registration information</li>
<li>Must comply with all applicable laws and regulations in your territory</li>
<li>Must not have been previously terminated from our program for violations</li>
<li>Must maintain active status through minimum purchase requirements</li>
</ul>
</div>
<Separator />
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Participant Obligations</h3>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<h4 className="font-medium text-gray-800">Business Conduct</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Represent products and business opportunity honestly</li>
<li>Comply with all marketing guidelines and policies</li>
<li>Maintain professional conduct in all business activities</li>
<li>Protect confidential and proprietary information</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-medium text-gray-800">Legal Compliance</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Obtain necessary business licenses and permits</li>
<li>Pay all applicable taxes on earnings</li>
<li>Follow local direct selling regulations</li>
<li>Maintain accurate records of business activities</li>
</ul>
</div>
</div>
</div>
<Separator />
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Commission Structure</h3>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<p className="text-sm text-green-800 mb-2">
<strong>Important:</strong> Commissions are based solely on actual product sales to end consumers.
Our compensation plan complies with applicable business partnership regulations.
</p>
<ul className="list-disc list-inside space-y-1 text-sm text-green-700">
<li>Personal sales commissions on direct product sales</li>
<li>Team bonuses based on group sales volume</li>
<li>Achievement rewards for reaching sales milestones</li>
<li>Leadership bonuses for developing successful teams</li>
</ul>
</div>
</div>
</CardContent>
</Card>
{/* Product Terms */}
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center">
<ShoppingCart className="h-5 w-5 mr-2 text-blue-600" />
Product Purchase Terms
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Product Quality & Authenticity</h3>
<p className="text-gray-700 leading-relaxed mb-4">
All Padmaaja Rasooi rice products are sourced from certified farms and undergo
rigorous quality control processes. We guarantee the authenticity and quality
of our products when purchased through authorized channels.
</p>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<h4 className="font-semibold text-amber-800 mb-2">Quality Assurance</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-amber-700">
<li>Premium grade rice sourced directly from certified farms</li>
<li>Strict quality control and testing procedures</li>
<li>Proper packaging and storage to maintain freshness</li>
<li>Clear labeling with nutritional information and origin details</li>
</ul>
</div>
</div>
<Separator />
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Pricing & Payment</h3>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<h4 className="font-medium text-gray-800">Pricing Policy</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>All prices are in Indian Rupees (INR)</li>
<li>Prices include applicable taxes unless stated otherwise</li>
<li>Shipping costs calculated separately</li>
<li>Prices subject to change without prior notice</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-medium text-gray-800">Payment Terms</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Payment required at time of order</li>
<li>Accepted payment methods as displayed</li>
<li>Secure payment processing via trusted gateways</li>
<li>No storage of payment card information</li>
</ul>
</div>
</div>
</div>
<Separator />
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Shipping & Delivery</h3>
<p className="text-gray-700 leading-relaxed mb-4">
We strive to process and ship orders within 1-2 business days. Delivery times
vary based on location and shipping method selected.
</p>
<div className="grid md:grid-cols-3 gap-4">
<div className="text-center p-4 border rounded-lg">
<div className="text-2xl font-bold text-green-600">1-2</div>
<div className="text-sm text-gray-600">Processing Days</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="text-2xl font-bold text-blue-600">3-7</div>
<div className="text-sm text-gray-600">Delivery Days</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="text-2xl font-bold text-purple-600">100%</div>
<div className="text-sm text-gray-600">Tracking Provided</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Prohibited Activities */}
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center">
<Gavel className="h-5 w-5 mr-2 text-red-600" />
Prohibited Activities
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-700 leading-relaxed">
The following activities are strictly prohibited and may result in immediate
termination of your account and/or legal action:
</p>
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-3">
<h4 className="font-semibold text-red-800">Marketing Violations</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Making false or misleading product claims</li>
<li>Promising unrealistic income potential</li>
<li>Using unauthorized marketing materials</li>
<li>Violating advertising guidelines</li>
<li>Spamming or unsolicited communications</li>
</ul>
</div>
<div className="space-y-3">
<h4 className="font-semibold text-red-800">Business Misconduct</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Cross-recruiting or poaching participants</li>
<li>Operating competing rice business partnerships</li>
<li>Manipulating commission structures</li>
<li>Selling counterfeit or unauthorized products</li>
<li>Violating territorial restrictions</li>
</ul>
</div>
<div className="space-y-3">
<h4 className="font-semibold text-red-800">Legal Violations</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Any illegal or fraudulent activities</li>
<li>Money laundering or financial crimes</li>
<li>Harassment or discriminatory behavior</li>
<li>Intellectual property infringement</li>
<li>Tax evasion or non-compliance</li>
</ul>
</div>
<div className="space-y-3">
<h4 className="font-semibold text-red-800">Technical Abuse</h4>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Hacking or unauthorized system access</li>
<li>Creating multiple fake accounts</li>
<li>Automated bot or script usage</li>
<li>Reverse engineering our software</li>
<li>Data scraping or unauthorized data collection</li>
</ul>
</div>
</div>
</CardContent>
</Card>
{/* Limitation of Liability */}
<Card className="mb-8">
<CardHeader>
<CardTitle>Limitation of Liability</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-700 leading-relaxed">
To the maximum extent permitted by law, Padmaaja Rasooi shall not be liable
for any indirect, incidental, special, consequential, or punitive damages,
including but not limited to:
</p>
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<ul className="list-disc list-inside space-y-2 text-sm text-red-800">
<li>Loss of profits, revenue, or business opportunities</li>
<li>Loss of data or confidential information</li>
<li>Business interruption or service unavailability</li>
<li>Third-party actions or claims</li>
<li>Any damages exceeding the amount paid to us in the preceding 12 months</li>
</ul>
</div>
<p className="text-sm text-gray-600 italic">
This limitation applies regardless of the legal theory of liability, whether in
contract, tort, strict liability, or otherwise.
</p>
</CardContent>
</Card>
{/* Termination */}
<Card className="mb-8">
<CardHeader>
<CardTitle>Termination</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid md:grid-cols-2 gap-6">
<div>
<h4 className="font-semibold text-gray-900 mb-3">Termination by You</h4>
<p className="text-sm text-gray-700 mb-2">
You may terminate your participation at any time by:
</p>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Providing written notice to us</li>
<li>Ceasing all business activities</li>
<li>Returning any confidential materials</li>
</ul>
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-3">Termination by Us</h4>
<p className="text-sm text-gray-700 mb-2">
We may terminate your account for:
</p>
<ul className="list-disc list-inside space-y-1 text-sm text-gray-700">
<li>Violation of these terms</li>
<li>Illegal or unethical conduct</li>
<li>Inactivity for extended periods</li>
<li>Non-payment of obligations</li>
</ul>
</div>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mt-4">
<h4 className="font-semibold text-yellow-800 mb-2">Effects of Termination</h4>
<p className="text-sm text-yellow-700">
Upon termination, all commissions earned but not yet paid may be forfeited,
access to business tools will be revoked, and you must cease all use of our
trademarks and business materials.
</p>
</div>
</CardContent>
</Card>
{/* Governing Law */}
<Card className="mb-8">
<CardHeader>
<CardTitle>Governing Law & Dispute Resolution</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-gray-700 leading-relaxed">
These Terms are governed by the laws of India. Any disputes arising from these
Terms or your use of our services shall be resolved through:
</p>
<div className="grid md:grid-cols-3 gap-4">
<div className="text-center p-4 border rounded-lg">
<div className="text-lg font-semibold text-blue-600 mb-2">Step 1</div>
<div className="text-sm text-gray-700">Direct Negotiation</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="text-lg font-semibold text-green-600 mb-2">Step 2</div>
<div className="text-sm text-gray-700">Mediation</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="text-lg font-semibold text-purple-600 mb-2">Step 3</div>
<div className="text-sm text-gray-700">Arbitration</div>
</div>
</div>
</CardContent>
</Card>
{/* Contact Information */}
<Card className="mb-8">
<CardHeader>
<CardTitle>Questions About These Terms</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-700 mb-4">
If you have any questions about these Terms of Service, please contact us:
</p>
<div className="bg-gray-50 border rounded-lg p-4 space-y-2">
<div className="flex items-center">
<span className="text-sm"><strong>Email:</strong> legal@padmaajarasooi.com</span>
</div>
<div className="flex items-center">
<span className="text-sm"><strong>Phone:</strong> +91 [Legal Department]</span>
</div>
<div className="flex items-start">
<span className="text-sm">
<strong>Address:</strong> Legal Department<br />
Padmaaja Rasooi Headquarters<br />
[Complete Address], India
</span>
</div>
</div>
</CardContent>
</Card>
{/* Final Notice */}
<Card>
<CardContent className="pt-6">
<div className="text-center">
<p className="text-sm text-gray-600 mb-4">
These Terms of Service constitute the entire agreement between you and Padmaaja Rasooi.
If any provision is found to be unenforceable, the remaining provisions will remain in full effect.
</p>
<Badge variant="secondary">
Effective Date: August 30, 2025
</Badge>
</div>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,426 @@
'use client'
import { motion } from 'framer-motion'
import Image from 'next/image'
import { Shield, Award, CheckCircle, FileText, Globe, Factory, Leaf, Users, Calendar, ExternalLink } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import PageHero from '@/components/sections/PageHero'
export default function CertificationsPage() {
const certifications = [
{
id: 1,
name: 'FSSAI License',
description: 'Food Safety and Standards Authority of India certification ensuring food safety and quality standards.',
category: 'Food Safety',
issuedBy: 'Food Safety and Standards Authority of India',
validity: 'Valid',
licenseNumber: '10825009000073',
icon: Shield,
color: 'bg-green-500',
status: 'Active'
},
{
id: 2,
name: 'ISO 9001:2015',
description: 'Quality Management System certification ensuring consistent quality in products and services.',
category: 'Quality Management',
issuedBy: 'International Organization for Standardization',
validity: '26 Sep 2024 25 Sep 2027',
licenseNumber: '305024092612Q',
icon: Award,
color: 'bg-blue-500',
status: 'Active'
},
{
id: 3,
name: 'ISO 14001:2015',
description: 'Environmental Management System certification for sustainable environmental practices.',
category: 'Environmental Management',
issuedBy: 'International Organization for Standardization',
validity: '26 Sep 2024 25 Sep 2027',
licenseNumber: '305024092613E',
icon: Leaf,
color: 'bg-green-600',
status: 'Active'
},
{
id: 4,
name: 'ISO 22000:2018',
description: 'Food Safety Management System certification throughout the food chain.',
category: 'Food Safety',
issuedBy: 'International Organization for Standardization',
validity: '26 Sep 2024 25 Sep 2027',
licenseNumber: '305024092615F',
icon: CheckCircle,
color: 'bg-purple-500',
status: 'Active'
},
{
id: 5,
name: 'ISO 45001:2018',
description: 'Occupational Health and Safety Management System ensuring workplace safety.',
category: 'Health & Safety',
issuedBy: 'International Organization for Standardization',
validity: '26 Sep 2024 25 Sep 2027',
licenseNumber: '305024092614HS',
icon: Shield,
color: 'bg-red-500',
status: 'Active'
},
{
id: 6,
name: 'HACCP Certification',
description: 'Hazard Analysis and Critical Control Points system for identifying and preventing food safety hazards.',
category: 'Food Safety',
issuedBy: 'Global Food Safety Initiative',
validity: '26 Sep 2024 25 Sep 2027',
licenseNumber: 'UQ-2024092617',
icon: FileText,
color: 'bg-orange-500',
status: 'Active'
},
{
id: 7,
name: 'IEC Code',
description: 'Import Export Code for international trade operations.',
category: 'Export Authorization',
issuedBy: 'Director General of Foreign Trade',
validity: 'Valid',
licenseNumber: 'AAPCP0216M',
icon: Globe,
color: 'bg-indigo-500',
status: 'Active'
},
{
id: 8,
name: 'LEI Code',
description: 'Legal Entity Identifier for global financial transactions and regulatory reporting.',
category: 'Financial Compliance',
issuedBy: 'Global Legal Entity Identifier Foundation',
validity: 'Valid till 14 Jan 2026',
licenseNumber: '3358009WKDVJJPAJMH24',
icon: FileText,
color: 'bg-cyan-500',
status: 'Active'
},
{
id: 9,
name: 'Startup Recognition',
description: 'Government recognition as a startup entity for various benefits and support.',
category: 'Government Recognition',
issuedBy: 'Department for Promotion of Industry and Internal Trade',
validity: 'Valid',
licenseNumber: 'DIPP179613',
icon: Factory,
color: 'bg-pink-500',
status: 'Active'
}
]
const complianceAreas = [
{
title: 'Food Safety Management',
description: 'Comprehensive food safety protocols ensuring consumer health and product integrity.',
icon: Shield,
certifications: ['FSSAI License', 'ISO 22000:2018', 'HACCP Certification']
},
{
title: 'Quality Management',
description: 'Rigorous quality control measures maintaining consistent product excellence.',
icon: Award,
certifications: ['ISO 9001:2015', 'ISO 22000:2018']
},
{
title: 'Environmental & Safety',
description: 'Environmental responsibility and workplace safety in all operations.',
icon: Leaf,
certifications: ['ISO 14001:2015', 'ISO 45001:2018']
},
{
title: 'Business Compliance',
description: 'Legal and regulatory compliance for domestic and international operations.',
icon: Globe,
certifications: ['IEC Code', 'LEI Code', 'Startup Recognition']
}
]
const achievements = [
{
year: '2024',
title: 'Best Food Processing Unit',
description: 'Awarded for excellence in food processing and quality standards.',
organization: 'State Food Processing Board'
},
{
year: '2023',
title: 'Export Excellence Award',
description: 'Recognition for outstanding contribution to food exports.',
organization: 'Export Promotion Council'
},
{
year: '2023',
title: 'Quality Leadership',
description: 'Acknowledged for maintaining highest quality standards.',
organization: 'Food Industry Association'
}
]
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50">
{/* Hero Section */}
<PageHero
title="Certifications"
subtitle="Our"
description="Committed to the highest standards of food safety, quality, and excellence. Our certifications demonstrate our dedication to delivering safe, premium food products."
badge={{
text: "Certified Excellence"
}}
icon={{
component: Shield,
bgColor: "bg-emerald-600"
}}
features={[
{ icon: CheckCircle, label: "9+ Active Certifications", color: "green" },
{ icon: Award, label: "ISO Standards Certified", color: "blue" },
{ icon: Globe, label: "Export & Trade Authorized", color: "purple" }
]}
backgroundGradient="from-emerald-600/10 to-blue-600/10"
titleGradient="from-emerald-600 to-blue-600"
/>
{/* Certifications Grid */}
<section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-slate-800 mb-4">
Active Certifications
</h2>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Our comprehensive certification portfolio ensures compliance with national and international standards.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{certifications.map((cert, index) => {
const IconComponent = cert.icon
return (
<motion.div
key={cert.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
>
<Card className="h-full hover:shadow-xl transition-all duration-300 group border-0 shadow-lg">
<CardHeader className="pb-4">
<div className="flex items-start justify-between mb-4">
<div className={`p-3 rounded-xl ${cert.color} shadow-lg`}>
<IconComponent className="w-6 h-6 text-white" />
</div>
<Badge
variant={cert.status === 'Active' ? 'default' : 'secondary'}
className={cert.status === 'Active' ? 'bg-green-100 text-green-700 border-green-200' : ''}
>
{cert.status}
</Badge>
</div>
<CardTitle className="text-xl font-bold text-slate-800 group-hover:text-emerald-600 transition-colors">
{cert.name}
</CardTitle>
<Badge variant="outline" className="w-fit text-xs">
{cert.category}
</Badge>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-slate-600 leading-relaxed">
{cert.description}
</p>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-500">Issued by:</span>
<span className="text-slate-700 font-medium text-end">{cert.issuedBy}</span>
</div>
{/* <div className="flex justify-between">
<span className="text-slate-500">Validity:</span>
<span className="text-slate-700 font-medium">{cert.validity}</span>
</div> */}
<div className="flex justify-between">
<span className="text-slate-500">License:</span>
<span className="text-slate-700 font-mono text-xs">{cert.licenseNumber}</span>
</div>
</div>
</CardContent>
</Card>
</motion.div>
)
})}
</div>
</div>
</section>
{/* Compliance Areas */}
<section className="py-20 bg-gradient-to-r from-slate-50 to-emerald-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-slate-800 mb-4">
Compliance Excellence
</h2>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Our comprehensive approach to compliance covers every aspect of food processing and quality assurance.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{complianceAreas.map((area, index) => {
const IconComponent = area.icon
return (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
>
<Card className="h-full text-center hover:shadow-lg transition-all duration-300 border-0 shadow-md bg-white/80 backdrop-blur-sm">
<CardContent className="p-6">
<div className="mb-6">
<div className="inline-flex p-4 bg-gradient-to-br from-emerald-500 to-blue-500 rounded-2xl shadow-lg">
<IconComponent className="w-8 h-8 text-white" />
</div>
</div>
<h3 className="text-xl font-bold text-slate-800 mb-3">
{area.title}
</h3>
<p className="text-slate-600 mb-4 leading-relaxed">
{area.description}
</p>
<div className="space-y-2">
{area.certifications.map((cert, certIndex) => (
<Badge key={certIndex} variant="outline" className="text-xs">
{cert}
</Badge>
))}
</div>
</CardContent>
</Card>
</motion.div>
)
})}
</div>
</div>
</section>
{/* Awards & Recognition */}
{/* <section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-slate-800 mb-4">
Awards & Recognition
</h2>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Our commitment to excellence has been recognized by industry leaders and regulatory bodies.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{achievements.map((achievement, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
>
<Card className="h-full hover:shadow-xl transition-all duration-300 border-0 shadow-lg bg-gradient-to-br from-white to-emerald-50">
<CardContent className="p-8 text-center">
<div className="mb-6">
<div className="inline-flex p-4 bg-gradient-to-br from-amber-400 to-orange-500 rounded-full shadow-lg">
<Award className="w-8 h-8 text-white" />
</div>
</div>
<Badge variant="outline" className="mb-4 text-amber-700 border-amber-200">
{achievement.year}
</Badge>
<h3 className="text-xl font-bold text-slate-800 mb-3">
{achievement.title}
</h3>
<p className="text-slate-600 mb-4 leading-relaxed">
{achievement.description}
</p>
<p className="text-sm font-medium text-emerald-600">
{achievement.organization}
</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section> */}
{/* Contact CTA */}
<section className="py-20 bg-gradient-to-r from-emerald-600 to-blue-600">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
>
<h2 className="text-3xl md:text-4xl font-bold text-white mb-6">
Need Certification Verification?
</h2>
<p className="text-xl text-emerald-100 mb-8 max-w-2xl mx-auto">
Contact us for detailed certification documents or verification of our compliance status.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
size="lg"
variant="secondary"
className="bg-white text-emerald-600 hover:bg-emerald-50"
asChild
>
<a href="https://4m5m4tx28rtva30c.public.blob.vercel-storage.com/media/2025-09-07/padmaaza-rasooi-profile" download="Padmaaja Rasooi Profile.pdf">
<FileText className="w-5 h-5 mr-2" />
Download Company Profile
</a>
</Button>
</div>
</motion.div>
</div>
</section>
</div>
)
}

View File

@@ -0,0 +1,499 @@
'use client'
import { motion } from 'framer-motion'
import Image from 'next/image'
import { Mail, Phone, MapPin, Calendar, Award, Target, Users, Quote, Briefcase, Star, ArrowRight, CheckCircle, TrendingUp } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import PageHero from '@/components/sections/PageHero'
export default function FounderPage() {
const founders = [
{
name: 'Rajeev Singh',
role: 'Founder & Chief Managing Director',
image: '/Rajeev Singh.jpg',
email: 'info@padmajarice.com',
phone: '+91 94757 58817',
description: 'Dynamic entrepreneur with 7+ years of experience in real estate. He successfully led Padmaja Group to a turnover of ₹200+ crores, showcasing his sharp business acumen and leadership skills.',
vision: 'Now, he brings the same vision and excellence to the food processing industry, aiming to build a trusted and scalable brand focused on quality, innovation, and customer satisfaction.',
achievements: [
'7+ years real estate experience',
'₹200+ crores group turnover',
'Dynamic business leadership',
'Strategic vision & execution'
]
},
{
name: 'Padmaja Singh',
role: 'Founder & Chairman',
image: '/Padmaja Singh.png',
description: 'Director at Padmaaja Rasooi Private Limited, with over 5 years of experience in managing large-scale business operations within the Padmaja Group. Her strong background in finance, project oversight, and administration has been instrumental in driving business efficiency and growth.',
vision: 'As a co-leader of Padmaaja Rasooi, she is committed to upholding the values of quality, sustainability, and customer satisfaction while steering the company toward long-term success in the food processing industry.',
achievements: [
'5+ years operations experience',
'Finance & project oversight',
'Business efficiency expert',
'Sustainability advocate'
]
}
]
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-orange-50">
{/* PageHero Section */}
<PageHero
title="Meet Our Visionary Leaders"
subtitle="Leadership Excellence"
description="Pioneering the future of authentic Indian cuisine through innovation, tradition, and unwavering commitment to excellence."
badge={{
text: "Leadership Excellence",
variant: "outline"
}}
icon={{
component: Star,
className: "w-8 h-8",
bgColor: "bg-gradient-to-br from-orange-500/20 to-orange-600/30"
}}
features={[
{
icon: Award,
label: "₹200+ Cr Group Turnover",
color: "orange"
},
{
icon: Calendar,
label: "12+ Years Combined Experience",
color: "emerald"
},
{
icon: Target,
label: "Founded Padmaaja Rasooi in 2023",
color: "blue"
}
]}
backgroundGradient="from-slate-900 via-slate-800 to-orange-900"
titleGradient="from-orange-400 to-orange-600"
alignment="center"
maxWidth="5xl"
/>
{/* Modern Founders Section */}
<section id="founders-section" className="py-32 bg-white relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-20"
>
<Badge className="mb-6 px-4 py-2 bg-orange-50 text-orange-700 border border-orange-200">
Leadership Team
</Badge>
<h2 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6">
The Minds Behind
<span className="block text-orange-600">Padmaaja Rasooi</span>
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Meet the visionary leaders who bring decades of business excellence
to India&apos;s food processing industry.
</p>
</motion.div>
{/* Founders Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16">
{founders.map((founder, index) => (
<motion.div
key={founder.name}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: index * 0.2 }}
viewport={{ once: true }}
className="group"
>
<Card className="border-0 shadow-lg hover:shadow-2xl transition-all duration-500 bg-gradient-to-br from-white to-gray-50 overflow-hidden relative">
<CardContent className="p-0">
{/* Founder Image Section */}
<div className="relative p-16 bg-gradient-to-br from-orange-50 to-emerald-50 overflow-hidden">
{/* Large Background Blobs */}
<div className="absolute top-0 left-0 w-64 h-64 bg-gradient-to-br from-orange-300/20 to-orange-400/30 rounded-full blur-3xl transform -translate-x-1/2 -translate-y-1/2"></div>
<div className="absolute bottom-0 right-0 w-48 h-48 bg-gradient-to-br from-emerald-300/20 to-emerald-400/30 rounded-full blur-3xl transform translate-x-1/2 translate-y-1/2"></div>
<div className="absolute top-1/2 left-1/2 w-32 h-32 bg-gradient-to-br from-orange-200/30 to-emerald-200/30 rounded-full blur-2xl transform -translate-x-1/2 -translate-y-1/2"></div>
<div className="flex justify-center relative z-10">
<div className="relative">
{/* Enhanced Background Glow */}
<div className="absolute -inset-8 bg-gradient-to-r from-orange-400/30 to-emerald-400/30 rounded-full blur-2xl group-hover:blur-3xl transition-all duration-500"></div>
<div className="absolute -inset-6 bg-gradient-to-r from-orange-300/20 to-emerald-300/20 rounded-full blur-xl"></div>
{/* Main Image - Much Larger */}
<div className="relative w-56 h-56 rounded-full overflow-hidden border-6 border-white shadow-2xl">
<Image
src={founder.image}
alt={founder.name}
width={224}
height={224}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
</div>
{/* Enhanced Status Badge */}
<div className="absolute -bottom-4 -right-4 w-20 h-20 bg-gradient-to-r from-orange-500 to-orange-600 rounded-full flex items-center justify-center shadow-xl border-4 border-white">
<Award className="h-10 w-10 text-white" />
</div>
</div>
</div>
</div>
{/* Content Section */}
<div className="p-8">
{/* Name & Role */}
<div className="text-center mb-6">
<h3 className="text-2xl font-bold text-gray-900 mb-2">
{founder.name}
</h3>
<Badge className="px-4 py-2 bg-orange-100 text-orange-800 border border-orange-200">
{founder.role}
</Badge>
</div>
{/* Contact Info */}
{founder.email && (
<div className="flex flex-col sm:flex-row gap-4 mb-6 p-4 bg-gray-50 rounded-lg">
<div className="flex items-center text-gray-600">
<Mail className="h-4 w-4 mr-2 text-orange-600" />
<span className="text-sm">{founder.email}</span>
</div>
<div className="flex items-center text-gray-600">
<Phone className="h-4 w-4 mr-2 text-orange-600" />
<span className="text-sm">{founder.phone}</span>
</div>
</div>
)}
{/* Description */}
<div className="space-y-4 mb-6">
<p className="text-gray-600 leading-relaxed">
{founder.description}
</p>
<p className="text-gray-800 leading-relaxed font-medium">
{founder.vision}
</p>
</div>
{/* Achievements */}
<div className="border-t border-gray-100 pt-6">
<h4 className="font-semibold text-lg mb-4 text-orange-700">
Key Achievements
</h4>
<div className="grid grid-cols-1 gap-3">
{founder.achievements.map((achievement, idx) => (
<div key={idx} className="flex items-start">
<CheckCircle className="h-5 w-5 text-emerald-500 mr-3 mt-0.5 flex-shrink-0" />
<span className="text-gray-700 text-sm leading-relaxed">{achievement}</span>
</div>
))}
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* Modern Business Story Section */}
<section className="py-32 bg-gradient-to-br from-gray-50 to-orange-50 relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-5">
<div className="h-full w-full" style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23F5873B' fill-opacity='0.4'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
}}></div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-20"
>
<Badge className="mb-6 px-4 py-2 bg-white text-gray-700 border border-gray-200 shadow-sm">
Our Story
</Badge>
<h2 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6 !leading-tight">
From Vision to
<span className="block text-orange-600">Reality</span>
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
A journey of transformation from successful real estate ventures to revolutionizing
India&apos;s food processing industry.
</p>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16">
{/* Business Legacy Card */}
<motion.div
initial={{ opacity: 0, x: -40 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<Card className="h-full border-0 shadow-xl bg-white hover:shadow-2xl transition-all duration-500">
<CardContent className="p-10">
{/* Header */}
<div className="flex items-center mb-8">
<div className="p-3 bg-gradient-to-r from-orange-100 to-orange-200 rounded-xl mr-4">
<TrendingUp className="h-8 w-8 text-orange-600" />
</div>
<div>
<h3 className="text-2xl font-bold text-gray-900">Business Legacy</h3>
<p className="text-orange-600 font-medium">200+ Crores Achieved</p>
</div>
</div>
{/* Content */}
<div className="space-y-6">
<p className="text-gray-600 leading-relaxed text-lg">
The <strong className="text-gray-900">Padmaja Group</strong>, under the visionary leadership of
Padmaja Singh, has achieved remarkable success with a turnover exceeding 100 crores.
</p>
<p className="text-gray-600 leading-relaxed">
Starting with real estate ventures, the group demonstrated exceptional business
acumen and strategic growth, building a foundation of trust and excellence.
</p>
<div className="bg-gradient-to-r from-orange-50 to-emerald-50 p-6 rounded-xl border-l-4 border-orange-500">
<p className="text-gray-700 font-medium italic">
&quot;Our transition from real estate to food processing represents our commitment
to creating sustainable value in essential industries that serve communities.&quot;
</p>
<p className="text-sm text-orange-600 mt-2 font-medium"> Founding Vision</p>
</div>
{/* Key Stats */}
<div className="grid grid-cols-2 gap-4 pt-4">
<div className="text-center p-4 bg-gray-50 rounded-lg">
<div className="text-2xl font-bold text-orange-600 mb-1">7+</div>
<div className="text-sm text-gray-600">Years Real Estate</div>
</div>
<div className="text-center p-4 bg-gray-50 rounded-lg">
<div className="text-2xl font-bold text-orange-600 mb-1">5+</div>
<div className="text-sm text-gray-600">Years Operations</div>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
{/* Vision & Mission Card */}
<motion.div
initial={{ opacity: 0, x: 40 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
viewport={{ once: true }}
>
<Card className="h-full border-0 shadow-xl bg-white hover:shadow-2xl transition-all duration-500">
<CardContent className="p-10">
{/* Header */}
<div className="flex items-center mb-8">
<div className="p-3 bg-gradient-to-r from-emerald-100 to-emerald-200 rounded-xl mr-4">
<Target className="h-8 w-8 text-emerald-600" />
</div>
<div>
<h3 className="text-2xl font-bold text-gray-900">Vision & Mission</h3>
<p className="text-emerald-600 font-medium">Future-Focused Leadership</p>
</div>
</div>
{/* Content */}
<div className="space-y-6">
<p className="text-gray-600 leading-relaxed text-lg">
To become <strong className="text-gray-900">India&apos;s most trusted name</strong> in authentic
food processing, preserving traditional flavors while embracing modern technology.
</p>
<p className="text-gray-600 leading-relaxed">
Our mission extends beyond business success we&apos;re building a legacy that honors
India&apos;s culinary heritage while creating opportunities for sustainable growth.
</p>
{/* Mission Points */}
<div className="space-y-4 pt-4">
<div className="flex items-start">
<CheckCircle className="h-6 w-6 text-emerald-500 mr-3 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-semibold text-gray-900 mb-1">Quality Excellence</h4>
<p className="text-gray-600">Quality-first approach in every product and process</p>
</div>
</div>
<div className="flex items-start">
<CheckCircle className="h-6 w-6 text-emerald-500 mr-3 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-semibold text-gray-900 mb-1">Sustainable Practices</h4>
<p className="text-gray-600">Ethical business practices that benefit all stakeholders</p>
</div>
</div>
<div className="flex items-start">
<CheckCircle className="h-6 w-6 text-emerald-500 mr-3 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-semibold text-gray-900 mb-1">Community Impact</h4>
<p className="text-gray-600">Empowering local communities and farmers across India</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
</div>
</div>
</section>
{/* Professional Call to Action Section */}
<section className="py-32 bg-gradient-to-br from-slate-900 via-slate-800 to-gray-900 relative overflow-hidden">
{/* Sophisticated Background Elements */}
<div className="absolute inset-0">
{/* Subtle geometric patterns */}
<div className="absolute top-20 left-20 w-72 h-72 bg-gradient-to-br from-orange-500/10 to-orange-600/20 rounded-full blur-3xl"></div>
<div className="absolute bottom-20 right-20 w-60 h-60 bg-gradient-to-br from-emerald-500/10 to-emerald-600/20 rounded-full blur-3xl"></div>
{/* Grid overlay */}
<div className="absolute inset-0 opacity-5">
<div className="h-full w-full" style={{
backgroundImage: `linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)`,
backgroundSize: '50px 50px'
}}></div>
</div>
</div>
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<Badge className="mb-8 px-6 py-3 bg-white/10 backdrop-blur-sm text-white border border-white/20 text-sm font-medium">
<Mail className="w-4 h-4 mr-2" />
Connect with Leadership
</Badge>
<h2 className="text-3xl lg:text-5xl font-bold text-white mb-8 !leading-tight">
Let&apos;s Build the Future
<span className="block bg-gradient-to-r from-orange-400 to-orange-600 bg-clip-text text-transparent">
Together
</span>
</h2>
<p className="text-xl text-slate-300 mb-12 max-w-3xl mx-auto leading-relaxed">
Ready to explore partnership opportunities, discuss our vision, or learn more
about Padmaaja Rasooi&apos;s journey? Our founders are always excited to connect with
like-minded individuals and organizations.
</p>
</motion.div>
{/* Enhanced Contact Cards */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-16">
{/* Email Card */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
viewport={{ once: true }}
>
<Card className="bg-white/5 backdrop-blur-sm border border-white/10 hover:bg-white/10 transition-all duration-300 group">
<CardContent className="p-8 text-center">
<div className="w-16 h-16 bg-gradient-to-r from-orange-500 to-orange-600 rounded-full flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
<Mail className="h-8 w-8 text-white" />
</div>
<h3 className="text-xl font-semibold text-white mb-3">Email Us</h3>
<p className="text-slate-400 mb-4">Send us a message anytime</p>
<p className="text-orange-400 font-medium">info@padmajarice.com</p>
</CardContent>
</Card>
</motion.div>
{/* Phone Card */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
viewport={{ once: true }}
>
<Card className="bg-white/5 backdrop-blur-sm border border-white/10 hover:bg-white/10 transition-all duration-300 group">
<CardContent className="p-8 text-center">
<div className="w-16 h-16 bg-gradient-to-r from-emerald-500 to-emerald-600 rounded-full flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
<Phone className="h-8 w-8 text-white" />
</div>
<h3 className="text-xl font-semibold text-white mb-3">Call Direct</h3>
<p className="text-slate-400 mb-4">Speak with our founder</p>
<p className="text-emerald-400 font-medium">+91 94757 58817</p>
</CardContent>
</Card>
</motion.div>
{/* Location Card */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
viewport={{ once: true }}
>
<Card className="bg-white/5 backdrop-blur-sm border border-white/10 hover:bg-white/10 transition-all duration-300 group">
<CardContent className="p-8 text-center">
<div className="w-16 h-16 bg-gradient-to-r from-purple-500 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
<MapPin className="h-8 w-8 text-white" />
</div>
<h3 className="text-xl font-semibold text-white mb-3">Visit Us</h3>
<p className="text-slate-400 mb-4">Based in India</p>
<p className="text-purple-400 font-medium">Schedule a Meeting</p>
</CardContent>
</Card>
</motion.div>
</div>
{/* Action Buttons */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5 }}
viewport={{ once: true }}
className="flex flex-col sm:flex-row gap-6 justify-center"
>
<Button
size="lg"
className="px-10 py-5 text-lg font-medium bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white border-0 shadow-2xl hover:shadow-orange-500/25 transition-all duration-300 transform hover:scale-105"
>
<Mail className="mr-3 h-6 w-6" />
Send Message
<ArrowRight className="ml-3 h-6 w-6" />
</Button>
<Button
size="lg"
variant="outline"
className="px-10 py-5 text-lg font-medium border-2 border-white/30 hover:bg-white hover:text-slate-900 backdrop-blur-sm transition-all duration-300 transform hover:scale-105"
>
<Phone className="mr-3 h-6 w-6" />
Schedule Call
</Button>
</motion.div>
</div>
</section>
</div>
)
}

918
app/(public)/about/page.tsx Normal file
View File

@@ -0,0 +1,918 @@
'use client'
import { motion } from 'framer-motion'
import Image from 'next/image'
import Link from 'next/link'
import { Users, Award, Target, Globe, Leaf, Shield, Heart, Factory, MapPin, Calendar, ArrowRight } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import ImageSlider from '@/components/ImageSlider'
export default function AboutPage() {
const sliderImages = [
{
src: 'https://images.pexels.com/photos/164504/pexels-photo-164504.jpeg',
alt: 'Rice Fields',
title: 'Premium Basmati Rice Fields'
},
{
src: 'https://images.pexels.com/photos/2804327/pexels-photo-2804327.jpeg',
alt: 'Rice Grains',
title: 'High Quality Rice Grains'
},
{
src: 'https://images.pexels.com/photos/8108170/pexels-photo-8108170.jpeg',
alt: 'Agriculture',
title: 'Modern Agricultural Practices'
},
{
src: '/farmer.png',
alt: 'Farmer',
title: 'Dedicated Farming Excellence'
},
// {
// src: '/lab.png',
// alt: 'Quality Control Lab',
// title: 'Advanced Quality Control Laboratory'
// },
{
src: '/factory.png',
alt: 'Manufacturing Facility',
title: 'Manufacturing Facility'
}
]
const features = [
{
icon: Leaf,
title: 'Premium Basmati Rice',
description: 'We source the finest Basmati rice from Haryana, known as the hub of premium quality Basmati rice.'
},
{
icon: Shield,
title: 'Quality Assurance',
description: 'FSSAI certified facilities with strict quality control measures ensuring food safety and excellence.'
},
{
icon: Heart,
title: 'Traditional Excellence',
description: 'Founded by experienced professionals with deep knowledge of rice processing and quality standards.'
},
{
icon: Factory,
title: 'Modern Processing',
description: 'Our manufacturing unit in Kaithal, Siwan, Haryana uses state-of-the-art technology for processing.'
}
]
const team = [
{
name: 'Rajeev Singh',
role: 'Founder & Director',
image: '/Rajeev Singh.jpg',
description: 'Ex-army professional who founded Padmaaja with extensive experience in real estate since 2018 and now leading FMCG operations.',
background: 'Ex-Army • Real Estate Expert • Visionary Leader'
},
{
name: 'Smt. Padmasha Singh',
role: 'Chairman',
image: '/Padmaja Singh.png',
description: 'Experienced leader providing strategic direction and ensuring the company maintains its commitment to quality and excellence.',
background: 'Strategic Leader • Quality Advocate • Industry Expert'
}
]
const milestones = [
{ year: '2018', title: 'Real Estate Journey', description: 'Started our business journey in real estate sector with successful operations' },
{ year: '2024 Q1', title: 'FMCG Expansion', description: 'Transitioned into FMCG products, focusing on premium Basmati rice processing' },
{ year: '2024 Q2', title: 'Manufacturing Setup', description: 'Established manufacturing unit in Kaithal, Siwan, Haryana - the hub of Basmati rice' },
{ year: '2025', title: 'Brand Launch', description: 'Launched Kashmina Basmati Rice brand with guaranteed quality standards' }
]
return (
<div className="min-h-screen">
{/* Image Slider Section */}
<section className="py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ImageSlider images={sliderImages} />
</div>
</section>
{/* Navigation Links */}
<section className="sticky top-[95px] z-50 py-4 bg-gray-50/95 backdrop-blur-sm border-b border-gray-200 shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-wrap justify-center gap-3">
<Link href="#who-we-are">
<Button variant="outline" className="hover:bg-orange-50 hover:border-orange-200">
Who We Are
</Button>
</Link>
<Link href="#mission-vision">
<Button variant="outline" className="hover:bg-orange-50 hover:border-orange-200">
Mission & Vision
</Button>
</Link>
<Link href="/about/founder">
<Button variant="outline" className="hover:bg-orange-50 hover:border-orange-200">
Our Founders
</Button>
</Link>
<Link href="#team">
<Button variant="outline" className="hover:bg-orange-50 hover:border-orange-200">
Our Team
</Button>
</Link>
<Link href="#features">
<Button variant="outline" className="hover:bg-orange-50 hover:border-orange-200">
Why Choose Us
</Button>
</Link>
<Link href="#journey">
<Button variant="outline" className="hover:bg-orange-50 hover:border-orange-200">
Our Journey
</Button>
</Link>
<Link href="#process">
<Button variant="outline" className="hover:bg-orange-50 hover:border-orange-200">
Our Process
</Button>
</Link>
</div>
</div>
</section>
{/* Who We Are Section */}
<section id="who-we-are" className="py-16 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center mb-12"
>
<h2 className="text-4xl font-bold text-gray-900 mb-4">
Who We Are
</h2>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left Column - Company Info */}
<div className="lg:col-span-2 space-y-6">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8 }}
className="space-y-6"
>
<div className="flex items-start space-x-3">
<MapPin className="w-6 h-6 text-orange-500 mt-1 flex-shrink-0" />
<div>
<h3 className="text-lg font-semibold text-orange-600 mb-2">
Based in Lucknow, Uttar Pradesh, India
</h3>
<p className="text-gray-700 leading-relaxed">
We are an India-based company headquartered in <strong>Lucknow, Uttar Pradesh</strong>.
Founded by an ex-army professional, <strong>Rajeev Singh (Founder and Director)</strong> and
his wife <strong>Smt. Padmasha Singh (Chairman)</strong>, we bring discipline, quality,
and excellence to everything we do.
</p>
</div>
</div>
<div className="bg-orange-50 rounded-lg p-6 border-l-4 border-orange-500">
<div className="flex items-center space-x-2 mb-4">
<Calendar className="w-5 h-5 text-orange-500" />
<h4 className="font-semibold text-gray-900">Our Journey</h4>
</div>
<div className="space-y-3">
<div className="flex items-center space-x-3">
<span className="bg-orange-500 text-white text-sm font-bold px-2 py-1 rounded">2018</span>
<span className="text-gray-700">Started our business journey in real estate</span>
</div>
<div className="flex items-center space-x-3">
<span className="bg-orange-500 text-white text-sm font-bold px-2 py-1 rounded">2024</span>
<span className="text-gray-700">Expanded into FMCG products, focusing on premium rice</span>
</div>
<div className="flex items-center space-x-3">
<span className="bg-orange-500 text-white text-sm font-bold px-2 py-1 rounded">Present</span>
<span className="text-gray-700">Leading manufacturer of Kashmina Basmati Rice</span>
</div>
</div>
</div>
<p className="text-gray-700 leading-relaxed">
Our manufacturing unit is strategically located in <strong>Kaithal, Siwan, Haryana</strong> -
the renowned hub of Basmati rice production. Haryana is globally recognized for producing
the finest quality Basmati rice, and our brand <strong>Kashmina Basmati Rice</strong>
represents this legacy of excellence.
</p>
<div className="bg-gradient-to-r from-orange-50 to-yellow-50 rounded-lg p-6 border border-orange-200">
<h4 className="font-bold text-gray-900 mb-3 text-lg">Our Promise</h4>
<p className="text-gray-800">
We assure and guarantee the highest quality standards in every grain of rice,
bringing you the authentic taste and aroma that Basmati is known for.
</p>
</div>
</motion.div>
</div>
{/* Right Column - Cards */}
<div className="space-y-6">
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="space-y-6"
>
{/* Manufacturing Excellence Card */}
<Card className="border-0 shadow-lg hover:shadow-xl transition-shadow">
<CardHeader className="pb-4">
<CardTitle className="text-xl font-bold text-gray-900 text-center">
Manufacturing Excellence
</CardTitle>
<div className="text-center">
<Badge className="bg-orange-100 text-orange-800 px-3 py-1 text-sm">
Kaithal, Siwan, Haryana
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-start space-x-3">
<div className="w-2 h-2 rounded-full bg-orange-500 mt-2 flex-shrink-0"></div>
<p className="text-sm text-gray-700">Located in Haryana - the heart of Basmati rice cultivation</p>
</div>
<div className="flex items-start space-x-3">
<div className="w-2 h-2 rounded-full bg-orange-500 mt-2 flex-shrink-0"></div>
<p className="text-sm text-gray-700">State-of-the-art processing facilities ensuring quality</p>
</div>
<div className="flex items-start space-x-3">
<div className="w-2 h-2 rounded-full bg-orange-500 mt-2 flex-shrink-0"></div>
<p className="text-sm text-gray-700">Advanced quality control laboratory for testing</p>
</div>
<div className="flex items-start space-x-3">
<div className="w-2 h-2 rounded-full bg-orange-500 mt-2 flex-shrink-0"></div>
<p className="text-sm text-gray-700">Hygienic packaging standards maintaining freshness</p>
</div>
</CardContent>
</Card>
{/* Kashmina Rice Card */}
<Card className="border-0 shadow-lg hover:shadow-xl transition-shadow bg-gradient-to-br from-emerald-50 to-orange-50">
<CardContent className="p-6">
<div className="flex items-center">
<div className="relative w-24 h-24 flex-shrink-0">
<Image
src="/rice_bags.png"
alt="Kashmina Basmati Rice"
fill
className="object-contain"
/>
</div>
<div className="flex-1">
<h4 className="font-bold text-xl text-gray-900 mb-3">Kashmina Basmati Rice</h4>
<p className="text-gray-700 leading-relaxed text-sm">
Our flagship brand represents the finest quality Basmati rice,
carefully selected, processed, and packaged to deliver the authentic
aroma, long grains, and exceptional taste that makes every meal special.
</p>
</div>
</div>
</CardContent>
</Card>
</motion.div>
</div>
</div>
</div>
</section>
{/* Mission & Vision Section */}
<section id="mission-vision" className="py-20 bg-gradient-to-br from-slate-50 to-emerald-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Mission */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8 }}
>
<Card className="h-full border-0 shadow-xl">
<CardHeader className="text-center pb-4">
<div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{backgroundColor: '#F5873B20'}}>
<Target className="h-8 w-8" style={{color: '#F5873B'}} />
</div>
<CardTitle className="text-3xl font-bold text-gray-900">Our Mission</CardTitle>
</CardHeader>
<CardContent>
<p className="text-lg text-gray-600 leading-relaxed text-center">
Our mission is very simple - we know that if we provide the best, you will collaborate with us as the best.
We think and work towards prevailing all over India. Our mission is to provide the best quality rice
for everyone at affordable cost, ensuring premium Basmati reaches every Indian kitchen.
</p>
</CardContent>
</Card>
</motion.div>
{/* Vision */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
>
<Card className="h-full border-0 shadow-xl">
<CardHeader className="text-center pb-4">
<div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 bg-emerald-100">
<Globe className="h-8 w-8 text-emerald-600" />
</div>
<CardTitle className="text-3xl font-bold text-gray-900">Our Vision</CardTitle>
</CardHeader>
<CardContent>
<p className="text-lg text-gray-600 leading-relaxed text-center">
We know that we are processing what you don&apos;t see, but you eat. Here our responsibility is paramount.
Our vision is to be the best in your eyes and within your budget - delivering exceptional quality
Kashmina Basmati Rice that exceeds expectations while remaining accessible to all.
</p>
</CardContent>
</Card>
</motion.div>
</div>
</div>
</section>
{/* Features Section */}
<section id="features" className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
Why Choose Padmaaja Rasooi?
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
We&apos;re committed to delivering authentic flavors through traditional recipes
and modern food processing excellence.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{features.map((feature, index) => (
<motion.div
key={feature.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: index * 0.1 }}
>
<Card className="h-full hover:shadow-xl transition-all duration-300 border-0 shadow-lg">
<CardContent className="p-6 text-center">
<div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{backgroundColor: '#F5873B20'}}>
<feature.icon className="h-8 w-8" style={{color: '#F5873B'}} />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">
{feature.title}
</h3>
<p className="text-gray-600">
{feature.description}
</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* Leadership Overview Section */}
<section className="py-20 bg-gradient-to-br from-slate-50 to-emerald-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
Our Leadership
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto mb-8">
Founded by experienced professionals with a vision to bring premium quality
Basmati rice from Haryana to every Indian household.
</p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="bg-white rounded-xl shadow-lg p-8 max-w-4xl mx-auto"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
<div className="space-y-4">
<h3 className="text-2xl font-bold text-gray-900">Meet Our Founders</h3>
<p className="text-gray-700 leading-relaxed">
<strong>Rajeev Singh (Founder & Director)</strong> - Ex-army professional with extensive
experience in real estate since 2018, now leading our FMCG operations with discipline and excellence.
</p>
<p className="text-gray-700 leading-relaxed">
<strong>Smt. Padmasha Singh (Chairman)</strong> - Experienced leader providing strategic
direction and ensuring our commitment to quality and excellence.
</p>
<Link href="/about/founder">
<Button className="bg-orange-500 hover:bg-orange-600 text-white mt-4">
Learn More About Our Founders
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="relative h-40 rounded-lg overflow-hidden">
<Image
src="/Rajeev Singh.jpg"
alt="Rajeev Singh"
fill
className="object-cover"
/>
</div>
<div className="relative h-40 rounded-lg overflow-hidden">
<Image
src="/Padmaja Singh.png"
alt="Smt. Padmasha Singh"
fill
className="object-cover"
/>
</div>
</div>
</div>
</motion.div>
</motion.div>
</div>
</section>
{/* Our Team Section */}
<section id="team" className="py-20 bg-gradient-to-br from-gray-50 via-white to-blue-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center mb-20"
>
<div className="inline-flex items-center bg-orange-100 text-orange-800 px-4 py-2 rounded-full text-sm font-medium mb-6">
<Users className="w-4 h-4 mr-2" />
Meet Our Team
</div>
<h2 className="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
Our Core Team
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto leading-relaxed">
Meet the dedicated professionals who ensure every grain of Kashmina Basmati Rice
meets our exceptional standards of quality and excellence.
</p>
</motion.div>
{/* Team Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 mb-20">
{[
{
name: "Abhishek Patel",
role: "HR",
department: "Human Resources",
description: "Manages our human resources and ensures smooth workforce operations across all departments.",
icon: "👔",
color: "from-blue-500 to-blue-600",
bgColor: "bg-blue-50",
borderColor: "border-blue-200"
},
{
name: "Atul Sahu",
role: "Account",
department: "Finance & Accounts",
description: "Manages financial operations, accounting, and ensures fiscal responsibility across the organization.",
icon: "💼",
color: "from-emerald-500 to-emerald-600",
bgColor: "bg-emerald-50",
borderColor: "border-emerald-200"
},
{
name: "Rahul Kumar",
role: "Lab",
department: "Quality Control Laboratory",
description: "Oversees all laboratory testing and quality analysis to maintain our premium standards.",
icon: "🔬",
color: "from-orange-500 to-orange-600",
bgColor: "bg-orange-50",
borderColor: "border-orange-200"
},
{
name: "Shivchandra",
role: "Processing Incharge",
department: "Rice Processing",
description: "Supervises rice processing operations and ensures efficient production workflows.",
icon: "🏭",
color: "from-purple-500 to-purple-600",
bgColor: "bg-purple-50",
borderColor: "border-purple-200"
},
{
name: "Sunil Awasthi",
role: "Marketing Head",
department: "Marketing & Sales",
description: "Leads marketing strategies and sales initiatives to expand our market presence.",
icon: "📈",
color: "from-teal-500 to-teal-600",
bgColor: "bg-teal-50",
borderColor: "border-teal-200"
},
{
name: "Sailesh Tiwari",
role: "Head of Operation",
department: "Operations Management",
description: "Manages overall operations and coordinates between different departments for seamless workflow.",
icon: "⚙️",
color: "from-indigo-500 to-indigo-600",
bgColor: "bg-indigo-50",
borderColor: "border-indigo-200"
},
{
name: "Ajad Singh",
role: "Head of Operation",
department: "Operations Management",
description: "Oversees operational excellence and ensures quality standards across all production processes.",
icon: "🎯",
color: "from-rose-500 to-rose-600",
bgColor: "bg-rose-50",
borderColor: "border-rose-200"
}
].map((member, index) => (
<motion.div
key={member.name}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
whileHover={{ y: -8, scale: 1.02 }}
className="group cursor-pointer"
>
<div className={`relative overflow-hidden rounded-2xl bg-white border ${member.borderColor} shadow-lg hover:shadow-2xl transition-all duration-300 h-full`}>
{/* Background Pattern */}
<div className={`absolute top-0 right-0 w-32 h-32 ${member.bgColor} rounded-full -translate-y-16 translate-x-16 opacity-20 group-hover:opacity-30 transition-opacity duration-300`} />
<div className="relative p-6">
{/* Icon */}
<div className={`w-14 h-14 rounded-xl bg-gradient-to-r ${member.color} flex items-center justify-center mb-4 text-white text-xl shadow-lg group-hover:scale-110 transition-transform duration-300`}>
{member.icon}
</div>
{/* Name & Role */}
<h3 className="text-lg font-bold text-gray-900 mb-1 group-hover:text-gray-700 transition-colors">
{member.name}
</h3>
<p className="text-orange-600 font-semibold text-sm mb-1">
{member.role}
</p>
<div className={`inline-block px-2 py-1 ${member.bgColor} text-xs font-medium rounded-full mb-3`}>
{member.department}
</div>
{/* Description */}
<p className="text-gray-600 text-sm leading-relaxed">
{member.description}
</p>
</div>
{/* Hover Effect Gradient */}
<div className={`absolute inset-0 bg-gradient-to-br ${member.color} opacity-0 group-hover:opacity-5 transition-opacity duration-300 rounded-2xl`} />
</div>
</motion.div>
))}
</div>
{/* Team Stats */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5 }}
className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-16"
>
{[
{ number: "7+", label: "Team Members", icon: "👥" },
{ number: "100%", label: "Dedication", icon: "⭐" },
{ number: "24/7", label: "Quality Focus", icon: "🎯" },
{ number: "200+ CR", label: "Revenue", icon: "💰" },
].map((stat, index) => (
<div key={stat.label} className="text-center">
<div className="bg-white rounded-xl p-6 shadow-lg border border-gray-100 hover:shadow-xl transition-shadow duration-300">
<div className="text-2xl mb-2">{stat.icon}</div>
<div className="text-2xl md:text-3xl font-bold text-orange-600 mb-1">{stat.number}</div>
<div className="text-gray-600 text-sm font-medium">{stat.label}</div>
</div>
</div>
))}
</motion.div>
{/* Banking Partner Banner */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.7 }}
className="relative overflow-hidden bg-gradient-to-r from-blue-600 via-blue-700 to-indigo-700 rounded-2xl p-8 md:p-12 text-white"
>
{/* Background Pattern */}
<div className="absolute inset-0 bg-gradient-to-r from-blue-600/20 to-transparent" />
<div className="absolute top-0 right-0 w-64 h-64 bg-white/5 rounded-full -translate-y-32 translate-x-32" />
<div className="absolute bottom-0 left-0 w-48 h-48 bg-white/5 rounded-full translate-y-24 -translate-x-24" />
<div className="relative text-center">
<div className="inline-flex items-center justify-center w-16 h-16 bg-white/20 backdrop-blur-sm rounded-full mb-6">
<div className="text-3xl">🏦</div>
</div>
<h3 className="text-2xl md:text-3xl font-bold mb-4">
Trusted Banking Partner
</h3>
<p className="text-xl mb-2 font-semibold text-blue-100">
State Bank of India, Kaithal
</p>
<p className="text-blue-100 max-w-2xl mx-auto leading-relaxed">
Our operations are backed by State Bank of India in Kaithal, Haryana,
ensuring financial stability and trust in all our business operations.
</p>
</div>
</motion.div>
</div>
</section>
{/* Journey & Milestones Section */}
<section id="journey" className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
Our Journey
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
From a small family kitchen to a trusted brand across India -
here&apos;s how we&apos;ve grown while staying true to our roots.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{milestones.map((milestone, index) => (
<motion.div
key={milestone.year}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: index * 0.1 }}
>
<Card className="h-full border-0 shadow-lg hover:shadow-xl transition-all duration-300">
<CardContent className="p-6 text-center">
<div className="text-3xl font-bold mb-2" style={{color: '#F5873B'}}>
{milestone.year}
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">
{milestone.title}
</h3>
<p className="text-gray-600 text-sm">
{milestone.description}
</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* Rice Processing Journey Section */}
<section id="process" className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
From Farm to Table
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Our meticulous 8-step process ensures every grain of Kashmina Basmati Rice
meets the highest standards of quality, purity, and freshness.
</p>
</motion.div>
<div className="relative">
{/* Process Flow */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{[
{
step: 1,
title: "Purchase",
description: "Premium paddy sourced directly from trusted farmers in Haryana",
icon: "🌾",
color: "from-green-400 to-green-600"
},
{
step: 2,
title: "Lab Testing",
description: "Rigorous quality checks for moisture, purity, and grain quality",
icon: "🔬",
color: "from-blue-400 to-blue-600"
},
{
step: 3,
title: "Drying Process",
description: "Controlled drying to optimal moisture levels for processing",
icon: "☀️",
color: "from-yellow-400 to-orange-500"
},
{
step: 4,
title: "Steaming",
description: "Traditional steaming process to enhance aroma and texture",
icon: "♨️",
color: "from-purple-400 to-purple-600"
},
{
step: 5,
title: "Sorting",
description: "Advanced sorting to separate premium long grains",
icon: "🔍",
color: "from-teal-400 to-teal-600"
},
{
step: 6,
title: "Quality Check",
description: "Final quality inspection ensuring consistency and excellence",
icon: "✅",
color: "from-emerald-400 to-emerald-600"
},
{
step: 7,
title: "Packaging",
description: "Hygienic packaging in food-grade materials for freshness",
icon: "📦",
color: "from-indigo-400 to-indigo-600"
},
{
step: 8,
title: "Labelling",
description: "Proper labelling with batch details and quality certifications",
icon: "🏷️",
color: "from-rose-400 to-rose-600"
}
].map((process, index) => (
<motion.div
key={process.step}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: index * 0.1 }}
className="relative"
>
{/* Connecting Line for larger screens */}
{index < 7 && (
<div className="hidden lg:block absolute top-16 left-full w-8 h-0.5 bg-gradient-to-r from-orange-300 to-orange-400 z-0" />
)}
<Card className="h-full border-0 shadow-lg hover:shadow-xl transition-all duration-300 relative z-10">
<CardContent className="p-6 text-center">
{/* Step Number */}
<div className={`w-12 h-12 rounded-full bg-gradient-to-r ${process.color} flex items-center justify-center mx-auto mb-4 text-white font-bold text-lg`}>
{process.step}
</div>
{/* Icon */}
<div className="text-4xl mb-3">
{process.icon}
</div>
{/* Title */}
<h3 className="text-xl font-semibold text-gray-900 mb-3">
{process.title}
</h3>
{/* Description */}
<p className="text-gray-600 text-sm leading-relaxed">
{process.description}
</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
{/* Quality Assurance Banner */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.8 }}
className="mt-16 bg-gradient-to-r from-orange-50 to-yellow-50 rounded-xl p-8 border-l-4 border-orange-500"
>
<div className="text-center">
<h3 className="text-2xl font-bold text-gray-900 mb-4">
🏆 Quality Guaranteed at Every Step
</h3>
<p className="text-gray-700 text-lg max-w-4xl mx-auto">
Our FSSAI-certified facility and experienced team ensure that every grain of
Kashmina Basmati Rice maintains the authentic aroma, perfect texture, and
exceptional taste that makes your meals truly special.
</p>
</div>
</motion.div>
</div>
</div>
</section>
{/* Values Section */}
<section className="py-20 bg-gradient-to-br from-slate-50 to-emerald-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
Our Core Values
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
The principles that guide everything we do at Padmaaja Rasooi.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<Card className="h-full border-0 shadow-lg">
<CardContent className="p-8 text-center">
<div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{backgroundColor: '#F5873B20'}}>
<Shield className="h-8 w-8" style={{color: '#F5873B'}} />
</div>
<h3 className="text-2xl font-semibold text-gray-900 mb-4">
Authenticity
</h3>
<p className="text-gray-600">
We stay true to traditional recipes and cooking methods, ensuring every product
delivers the authentic taste that connects you to India&apos;s rich culinary heritage.
</p>
</CardContent>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.1 }}
>
<Card className="h-full border-0 shadow-lg">
<CardContent className="p-8 text-center">
<div className="w-16 h-16 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Leaf className="h-8 w-8 text-emerald-600" />
</div>
<h3 className="text-2xl font-semibold text-gray-900 mb-4">
Quality
</h3>
<p className="text-gray-600">
From sourcing the finest ingredients to maintaining strict quality controls,
we ensure every product meets the highest standards of safety and excellence.
</p>
</CardContent>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
>
<Card className="h-full border-0 shadow-lg">
<CardContent className="p-8 text-center">
<div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{backgroundColor: '#F5873B20'}}>
<Heart className="h-8 w-8" style={{color: '#F5873B'}} />
</div>
<h3 className="text-2xl font-semibold text-gray-900 mb-4">
Family First
</h3>
<p className="text-gray-600">
We believe food brings families together. Every product is crafted with the same
love and care we put into our own family&apos;s meals.
</p>
</CardContent>
</Card>
</motion.div>
</div>
</div>
</section>
</div>
)
}

View File

@@ -0,0 +1,516 @@
'use client'
import { motion } from 'framer-motion'
import { Truck, Package, Users, Factory, Globe, Shield, Clock, CheckCircle, Calculator, Phone, Mail, Download, ArrowRight, Warehouse, Scale, Target } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import PageHero from '@/components/sections/PageHero'
export default function BulkSupplyPage() {
const bulkProducts = [
{
id: 1,
name: 'Premium Basmati Rice',
category: 'Rice & Grains',
minOrder: '25 MT',
pricing: '₹85-95/kg',
packaging: '25kg, 50kg PP Bags',
specifications: 'Extra Long Grain, Aged 2+ Years',
image: '/api/placeholder/300/200',
features: ['Export Quality', 'Sortex Cleaned', 'Moisture <12%']
},
{
id: 2,
name: 'Golden Sella Basmati',
category: 'Rice & Grains',
minOrder: '20 MT',
pricing: '₹78-88/kg',
packaging: '25kg, 50kg PP Bags',
specifications: 'Parboiled, Golden Color',
image: '/api/placeholder/300/200',
features: ['Non-Sticky', 'Longer Shelf Life', 'Uniform Color']
},
{
id: 5,
name: 'Wheat Flour',
category: 'Flour & Grains',
minOrder: '50 MT',
pricing: '₹28-35/kg',
packaging: '25kg, 50kg PP Bags',
specifications: 'Protein 11-12%, Ash <0.60%',
image: '/api/placeholder/300/200',
features: ['Fortified', 'Pest Free', 'Uniform Texture']
},
]
const services = [
{
icon: Factory,
title: 'Custom Processing',
description: 'Tailored processing solutions to meet your specific requirements and quality standards.',
features: ['Custom Grading', 'Special Packaging', 'Quality Specifications']
},
{
icon: Truck,
title: 'Logistics Support',
description: 'End-to-end logistics solutions from our facility to your destination worldwide.',
features: ['Container Loading', 'Documentation', 'Tracking Support']
},
{
icon: Shield,
title: 'Quality Assurance',
description: 'Comprehensive quality control with certifications and testing at every stage.',
features: ['Pre-shipment Inspection', 'Quality Certificates', 'Sample Approval']
},
{
icon: Globe,
title: 'Export Services',
description: 'Complete export facilitation with international compliance and documentation.',
features: ['Export Documentation', 'Phytosanitary Certificates', 'COO Certificates']
}
]
const advantages = [
{
icon: Scale,
title: 'Competitive Pricing',
description: 'Direct from manufacturer pricing with volume-based discounts.',
value: 'Up to 15% Savings'
},
{
icon: Clock,
title: 'Quick Turnaround',
description: 'Fast processing and delivery to meet your timeline requirements.',
value: '7-14 Days Delivery'
},
{
icon: CheckCircle,
title: 'Quality Guarantee',
description: 'Assured quality with money-back guarantee on specifications.',
value: '100% Quality Assurance'
},
{
icon: Target,
title: 'Flexible Terms',
description: 'Customizable payment and delivery terms for long-term partnerships.',
value: 'Flexible Payment Terms'
}
]
const specifications = [
{
parameter: 'Minimum Order Quantity',
value: '3-50 MT (Product Dependent)',
icon: Package
},
{
parameter: 'Payment Terms',
value: 'LC, TT, or Advance Payment',
icon: Calculator
},
{
parameter: 'Delivery Time',
value: '7-21 Days from Order Confirmation',
icon: Clock
},
{
parameter: 'Packaging Options',
value: 'PP Bags, Jute Bags, Cartons, Bulk',
icon: Warehouse
},
{
parameter: 'Quality Standards',
value: 'FSSAI, ISO 22000, HACCP Certified',
icon: Shield
},
{
parameter: 'Documentation',
value: 'COA, COO, Phytosanitary Available',
icon: CheckCircle
}
]
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50">
{/* Hero Section */}
<PageHero
title="Supply Solutions"
subtitle="Bulk"
description="Premium quality food products in bulk quantities for distributors, retailers, and food manufacturers. Direct from source with competitive pricing and reliable supply chain."
badge={{
text: "Wholesale Solutions"
}}
icon={{
component: Warehouse,
bgColor: "bg-blue-600"
}}
features={[
{ icon: Package, label: "3+ MT Minimum Orders", color: "blue" },
{ icon: Globe, label: "Global Shipping", color: "green" },
{ icon: Shield, label: "Quality Certified", color: "purple" }
]}
actions={[
{
label: "Get Quote",
icon: Calculator,
variant: "primary"
},
{
label: "Download Catalog",
icon: Download,
variant: "secondary"
}
]}
backgroundGradient="from-blue-600/10 to-emerald-600/10"
titleGradient="from-blue-600 to-emerald-600"
/>
{/* Bulk Products Grid */}
<section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-slate-800 mb-4">
Bulk Product Portfolio
</h2>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Premium quality products available in bulk quantities with competitive pricing and flexible terms.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{bulkProducts.map((product, index) => (
<motion.div
key={product.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
>
<Card className="h-full hover:shadow-xl transition-all duration-300 group border-0 shadow-lg overflow-hidden">
<div className="relative h-48 bg-gradient-to-br from-slate-100 to-slate-200">
<div className="absolute inset-0 flex items-center justify-center">
<Package className="w-16 h-16 text-slate-400" />
</div>
<Badge className="absolute top-3 left-3 bg-blue-600">
{product.category}
</Badge>
</div>
<CardHeader className="pb-4">
<CardTitle className="text-xl font-bold text-slate-800 group-hover:text-blue-600 transition-colors">
{product.name}
</CardTitle>
<div className="flex justify-between items-center">
<span className="text-sm text-slate-500">Min Order:</span>
<Badge variant="outline" className="text-blue-700 border-blue-200">
{product.minOrder}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-slate-500 block">Pricing Range</span>
<span className="font-semibold text-emerald-600">{product.pricing}</span>
</div>
<div>
<span className="text-slate-500 block">Packaging</span>
<span className="font-medium text-slate-700">{product.packaging}</span>
</div>
</div>
<div>
<span className="text-slate-500 text-sm block mb-2">Specifications</span>
<p className="text-sm text-slate-600">{product.specifications}</p>
</div>
<div className="flex flex-wrap gap-1">
{product.features.map((feature, idx) => (
<Badge key={idx} variant="secondary" className="text-xs">
{feature}
</Badge>
))}
</div>
<Button className="w-full mt-4 bg-blue-600 hover:bg-blue-700 text-white">
Request Quote
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* Services Section */}
<section className="py-20 bg-gradient-to-r from-slate-50 to-blue-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-slate-800 mb-4">
Comprehensive Services
</h2>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
End-to-end support for your bulk procurement needs with professional service excellence.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{services.map((service, index) => {
const IconComponent = service.icon
return (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
>
<Card className="h-full text-center hover:shadow-lg transition-all duration-300 border-0 shadow-md bg-white/80 backdrop-blur-sm">
<CardContent className="p-6">
<div className="mb-6">
<div className="inline-flex p-4 bg-gradient-to-br from-blue-500 to-emerald-500 rounded-2xl shadow-lg">
<IconComponent className="w-8 h-8 text-white" />
</div>
</div>
<h3 className="text-xl font-bold text-slate-800 mb-3">
{service.title}
</h3>
<p className="text-slate-600 mb-4 leading-relaxed">
{service.description}
</p>
<div className="space-y-2">
{service.features.map((feature, featureIndex) => (
<div key={featureIndex} className="flex items-center justify-center">
<CheckCircle className="w-4 h-4 text-green-500 mr-2" />
<span className="text-sm text-slate-600">{feature}</span>
</div>
))}
</div>
</CardContent>
</Card>
</motion.div>
)
})}
</div>
</div>
</section>
{/* Advantages Section */}
<section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-slate-800 mb-4">
Why Choose Our Bulk Supply?
</h2>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Competitive advantages that make us your preferred bulk supply partner.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{advantages.map((advantage, index) => {
const IconComponent = advantage.icon
return (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
>
<Card className="h-full text-center hover:shadow-xl transition-all duration-300 border-0 shadow-lg bg-gradient-to-br from-white to-blue-50">
<CardContent className="p-8">
<div className="mb-6">
<div className="inline-flex p-4 bg-gradient-to-br from-blue-500 to-purple-500 rounded-full shadow-lg">
<IconComponent className="w-8 h-8 text-white" />
</div>
</div>
<h3 className="text-xl font-bold text-slate-800 mb-3">
{advantage.title}
</h3>
<p className="text-slate-600 mb-4 leading-relaxed">
{advantage.description}
</p>
<Badge className="bg-blue-100 text-blue-700 border-blue-200">
{advantage.value}
</Badge>
</CardContent>
</Card>
</motion.div>
)
})}
</div>
</div>
</section>
{/* Specifications Table */}
<section className="py-20 bg-gradient-to-r from-slate-50 to-emerald-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-slate-800 mb-4">
Order Specifications
</h2>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Complete order guidelines and specifications for bulk procurement.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{specifications.map((spec, index) => {
const IconComponent = spec.icon
return (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
>
<Card className="hover:shadow-lg transition-all duration-300 border-0 shadow-md bg-white">
<CardContent className="p-6">
<div className="flex items-start space-x-4">
<div className="p-3 bg-gradient-to-br from-emerald-500 to-blue-500 rounded-xl shadow-lg">
<IconComponent className="w-6 h-6 text-white" />
</div>
<div className="flex-1">
<h3 className="font-bold text-slate-800 mb-2">
{spec.parameter}
</h3>
<p className="text-slate-600 font-medium">
{spec.value}
</p>
</div>
</div>
</CardContent>
</Card>
</motion.div>
)
})}
</div>
</div>
</section>
{/* Quote Request Form */}
<section className="py-20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="text-3xl md:text-4xl font-bold text-slate-800 mb-4">
Request Bulk Quote
</h2>
<p className="text-lg text-slate-600">
Get competitive pricing for your bulk requirements. Our team will respond within 24 hours.
</p>
</motion.div>
<Card className="shadow-2xl border-0 bg-gradient-to-br from-white to-slate-50">
<CardContent className="p-8">
<form className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="company">Company Name *</Label>
<Input id="company" placeholder="Your Company Name" />
</div>
<div className="space-y-2">
<Label htmlFor="contact">Contact Person *</Label>
<Input id="contact" placeholder="Contact Person Name" />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="email">Email Address *</Label>
<Input id="email" type="email" placeholder="email@company.com" />
</div>
<div className="space-y-2">
<Label htmlFor="phone">Phone Number *</Label>
<Input id="phone" placeholder="+91 98765 43210" />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="product">Product Category</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="Select Category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="rice">Rice & Grains</SelectItem>
<SelectItem value="spices">Spices</SelectItem>
<SelectItem value="flour">Flour & Grains</SelectItem>
<SelectItem value="pulses">Pulses</SelectItem>
<SelectItem value="mixed">Mixed Products</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="quantity">Estimated Quantity (MT)</Label>
<Input id="quantity" placeholder="e.g., 25 MT" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="requirements">Detailed Requirements</Label>
<Textarea
id="requirements"
placeholder="Please specify products, quantities, quality requirements, delivery timeline, and any special instructions..."
rows={4}
/>
</div>
<Button type="submit" className="w-full bg-blue-600 text-white hover:bg-blue-700 text-base py-3">
<Calculator className="w-5 h-5 mr-2" />
Submit Quote Request
</Button>
</form>
</CardContent>
</Card>
</div>
</section>
</div>
)
}

View File

@@ -0,0 +1,77 @@
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Shopping Cart - Padmaaja Rasooi | Review Your Premium Rice Products',
description: 'Review your selected premium rice products in your shopping cart. Secure checkout for the finest quality rice varieties from Padmaaja Rasooi.',
keywords: ['shopping cart', 'padmaaja rasooi', 'rice products', 'checkout', 'premium rice', 'secure shopping', 'cart review', 'order summary'],
openGraph: {
title: 'Shopping Cart - Padmaaja Rasooi',
description: 'Review your selected premium rice products and proceed to secure checkout.',
type: 'website',
images: ['/cart-icon.png'],
},
twitter: {
card: 'summary',
title: 'Shopping Cart - Padmaaja Rasooi',
description: 'Review your selected premium rice products and proceed to secure checkout.',
},
robots: {
index: false, // Don't index cart pages
follow: true,
noarchive: true,
nosnippet: true,
},
alternates: {
canonical: '/cart',
},
other: {
'cart-page': 'true',
'secure-checkout': 'enabled',
'page-type': 'ecommerce-cart',
},
}
export default function CartLayout({
children,
}: {
children: React.ReactNode
}) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "WebPage",
"name": "Shopping Cart - Padmaaja Rasooi",
"description": "Review your selected premium rice products and proceed to secure checkout.",
"url": "/cart",
"isPartOf": {
"@type": "WebSite",
"name": "Padmaaja Rasooi",
"url": "/"
},
"potentialAction": {
"@type": "CheckoutAction",
"target": "/checkout"
}
}
return (
<>
{/* Enhanced structured data for cart */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, '\\u003c')
}}
/>
<main role="main" aria-label="Shopping Cart">
<div className="cart-container" itemScope itemType="https://schema.org/ShoppingCart">
<header className="sr-only">
<h1>Shopping Cart</h1>
</header>
<section aria-label="Cart items">
{children}
</section>
</div>
</main>
</>
)
}

281
app/(public)/cart/page.tsx Normal file
View File

@@ -0,0 +1,281 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { ArrowLeft, ShoppingCart, Trash2, Plus, Minus, Heart } from 'lucide-react'
import { cartManager } from '@/lib/cart'
import { toast } from 'sonner'
import Link from 'next/link'
import Image from 'next/image'
import { motion } from 'framer-motion'
import PageHero from '@/components/sections/PageHero'
interface CartItem {
id: string
name: string
price: number
quantity: number
image: string | null
}
export default function CartPage() {
const router = useRouter()
const [cart, setCart] = useState<CartItem[]>([])
const [updating, setUpdating] = useState<string | null>(null)
const loadCart = useCallback(() => {
const cartItems = cartManager.getCart()
setCart(cartItems || [])
}, [])
useEffect(() => {
loadCart()
const handleCartUpdate = () => loadCart()
window.addEventListener('cartUpdated', handleCartUpdate)
return () => {
window.removeEventListener('cartUpdated', handleCartUpdate)
}
}, [loadCart])
const updateQuantity = async (productId: string, newQuantity: number) => {
setUpdating(productId)
cartManager.updateQuantity(productId, newQuantity)
loadCart()
setUpdating(null)
}
const removeItem = (productId: string) => {
cartManager.removeFromCart(productId)
toast.success('Item removed from cart')
loadCart()
}
const getTotalPrice = () => {
return cartManager.getTotalPrice()
}
const getTotalItems = () => {
return cart.reduce((sum, item) => sum + item.quantity, 0)
}
if (cart.length === 0) {
return (
<div className="min-h-screen bg-white">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="mb-8">
<Button variant="ghost" asChild className="mb-6 text-gray-600 hover:text-gray-900">
<Link href="/products">
<ArrowLeft className="h-4 w-4 mr-2" />
Continue Shopping
</Link>
</Button>
<h1 className="text-4xl font-bold text-gray-900 mb-2">Shopping Cart</h1>
<p className="text-gray-600">Your cart is currently empty</p>
</div>
<div className="text-center py-16">
<div className="w-24 h-24 mx-auto mb-6 bg-gray-100 rounded-full flex items-center justify-center">
<ShoppingCart className="h-12 w-12 text-gray-400" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">Your cart is empty</h3>
<p className="text-gray-600 mb-8 max-w-md mx-auto">
Looks like you haven&apos;t added anything to your cart yet. Start shopping to fill it up!
</p>
<Button size="lg" asChild className="bg-black text-white hover:bg-gray-800">
<Link href="/products">Start Shopping</Link>
</Button>
</div>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="mb-8">
<Button variant="ghost" asChild className="mb-6 text-gray-600 hover:text-gray-900">
<Link href="/products">
<ArrowLeft className="h-4 w-4 mr-2" />
Continue Shopping
</Link>
</Button>
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-bold text-gray-900 mb-2">Shopping Cart</h1>
<p className="text-gray-600">{getTotalItems()} {getTotalItems() === 1 ? 'item' : 'items'} in your cart</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 xl:grid-cols-3 gap-12">
{/* Cart Items */}
<div className="xl:col-span-2 space-y-6">
{cart.map((item, index) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.05 }}
className="group"
>
<div className="bg-white border border-gray-200 rounded-2xl p-6 hover:border-gray-300 transition-colors duration-200">
<div className="flex items-start gap-6">
{/* Product Image */}
<div className="relative flex-shrink-0">
<Image
src={item.image || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={item.name}
width={120}
height={120}
className="rounded-xl object-cover bg-gray-100"
/>
</div>
{/* Product Details */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-1 line-clamp-2">
{item.name}
</h3>
<p className="text-2xl font-bold text-gray-900">
{(item.price || 0).toFixed(2)}
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
className="text-gray-400 hover:text-gray-600 opacity-0 group-hover:opacity-100 transition-opacity"
>
<Heart className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => removeItem(item.id)}
className="text-gray-400 hover:text-red-600"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
{/* Quantity Controls */}
<div className="flex items-center justify-between">
<div className="flex items-center bg-gray-100 rounded-full">
<Button
variant="ghost"
size="sm"
onClick={() => updateQuantity(item.id, item.quantity - 1)}
disabled={item.quantity <= 1 || updating === item.id}
className="h-10 w-10 rounded-full hover:bg-gray-200"
>
<Minus className="h-4 w-4" />
</Button>
<span className="w-12 text-center font-semibold text-gray-900">
{updating === item.id ? '...' : item.quantity}
</span>
<Button
variant="ghost"
size="sm"
onClick={() => updateQuantity(item.id, item.quantity + 1)}
disabled={updating === item.id}
className="h-10 w-10 rounded-full hover:bg-gray-200"
>
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="text-right">
<p className="text-lg font-bold text-gray-900">
{((item.price || 0) * item.quantity).toFixed(2)}
</p>
<p className="text-sm text-gray-500">
{(item.price || 0).toFixed(2)} each
</p>
</div>
</div>
</div>
</div>
</div>
</motion.div>
))}
</div>
{/* Order Summary */}
<div className="xl:col-span-1">
<div className="sticky top-8">
<div className="bg-gray-50 rounded-2xl p-8 border border-gray-200">
<h2 className="text-xl font-bold text-gray-900 mb-6">Order Summary</h2>
<div className="space-y-4 mb-6">
<div className="flex justify-between text-gray-600">
<span>Subtotal ({getTotalItems()} items)</span>
<span className="font-semibold">{getTotalPrice().toFixed(2)}</span>
</div>
<div className="flex justify-between text-gray-600">
<span>Shipping</span>
<span className="font-semibold text-green-600">Free</span>
</div>
<div className="flex justify-between text-gray-600">
<span>Tax</span>
<span className="font-semibold">Calculated at checkout</span>
</div>
<Separator className="my-4" />
<div className="flex justify-between items-center">
<span className="text-lg font-bold text-gray-900">Total</span>
<span className="text-2xl font-bold text-gray-900">{getTotalPrice().toFixed(2)}</span>
</div>
</div>
<div className="space-y-3">
<Button
className="w-full h-12 bg-black text-white hover:bg-gray-800 rounded-xl text-base font-semibold"
asChild
>
<Link href="/checkout">
Proceed to Checkout
</Link>
</Button>
<Button
variant="outline"
className="w-full h-12 border-gray-300 hover:bg-gray-50 rounded-xl text-base"
asChild
>
<Link href="/products">
Continue Shopping
</Link>
</Button>
</div>
{/* Security Badge */}
<div className="mt-6 pt-6 border-t border-gray-200">
<div className="flex items-center justify-center text-sm text-gray-500">
<div className="w-4 h-4 bg-green-500 rounded-full mr-2"></div>
Secure checkout powered by Razorpay
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,740 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Badge } from '@/components/ui/badge'
import { ArrowLeft, CreditCard, Truck, Plus, MapPin } from 'lucide-react'
import { cartManager } from '@/lib/cart'
import { toast } from 'sonner'
import Link from 'next/link'
import Image from 'next/image'
interface CartItem {
id: string
name: string
price: number
quantity: number
image: string | null
}
interface Address {
id: string
firstName: string
lastName: string
company?: string
address1: string
address2?: string
city: string
state: string
zipCode: string
country: string
phone?: string
isDefault: boolean
type: 'HOME' | 'WORK' | 'OTHER'
}
declare global {
interface Window {
Razorpay: any;
}
}
export default function CheckoutPage() {
const router = useRouter()
const { data: session, status } = useSession()
const [cart, setCart] = useState<CartItem[]>([])
const [addresses, setAddresses] = useState<Address[]>([])
const [selectedAddressId, setSelectedAddressId] = useState<string>('')
const [loading, setLoading] = useState(false)
const [useNewAddress, setUseNewAddress] = useState(false)
const [formData, setFormData] = useState({
email: '',
firstName: '',
lastName: '',
address: '',
city: '',
state: '',
zipCode: '',
phone: ''
})
const [saveAddress, setSaveAddress] = useState(false)
const [processingPayment, setProcessingPayment] = useState(false)
const loadCart = useCallback(() => {
const cartItems = cartManager.getCart()
setCart(cartItems || [])
if (cartItems.length === 0) {
router.push('/products')
}
}, [router])
const fetchAddresses = useCallback(async () => {
if (status !== 'authenticated') return
try {
const response = await fetch('/api/user/addresses')
if (response.ok) {
const data = await response.json()
const userAddresses = data.addresses || []
setAddresses(userAddresses)
// Auto-select default address
const defaultAddress = userAddresses.find((addr: Address) => addr.isDefault)
if (defaultAddress) {
setSelectedAddressId(defaultAddress.id)
} else if (userAddresses.length > 0) {
setSelectedAddressId(userAddresses[0].id)
} else {
setUseNewAddress(true)
}
}
} catch (error) {
console.error('Error fetching addresses:', error)
setUseNewAddress(true)
}
}, [status])
const fetchUserProfile = useCallback(async () => {
if (status !== 'authenticated') return
try {
const response = await fetch('/api/user/profile')
if (response.ok) {
const userData = await response.json()
// Prefill form with user data
setFormData(prev => ({
...prev,
email: userData.email || session?.user?.email || '',
firstName: userData.name?.split(' ')[0] || session?.user?.name?.split(' ')[0] || '',
lastName: userData.name?.split(' ').slice(1).join(' ') || session?.user?.name?.split(' ').slice(1).join(' ') || '',
phone: userData.phone || ''
}))
}
} catch (error) {
console.error('Error fetching user profile:', error)
// Fallback to session data
if (session?.user) {
setFormData(prev => ({
...prev,
email: session.user.email || '',
firstName: session.user.name?.split(' ')[0] || '',
lastName: session.user.name?.split(' ').slice(1).join(' ') || ''
}))
}
}
}, [status, session?.user])
useEffect(() => {
loadCart()
}, [loadCart])
useEffect(() => {
if (status === 'authenticated') {
fetchAddresses()
fetchUserProfile()
} else if (status === 'unauthenticated') {
// For guest checkout, keep form empty
setUseNewAddress(true)
}
}, [status, fetchAddresses, fetchUserProfile])
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
const getSubtotal = () => {
return cart.reduce((sum, item) => sum + (item.price * item.quantity), 0)
}
const getTotal = () => {
return getSubtotal() // Add tax, shipping, etc. here if needed
}
const getSelectedAddress = () => {
return addresses.find(addr => addr.id === selectedAddressId)
}
const handlePlaceOrder = async () => {
// Get shipping address data
let shippingAddress
let savedAddressId = null
if (useNewAddress) {
// Validate new address form
const requiredFields = ['email', 'firstName', 'lastName', 'address', 'city', 'state', 'zipCode', 'phone']
const missingFields = requiredFields.filter(field => !formData[field as keyof typeof formData])
if (missingFields.length > 0) {
toast.error('Please fill in all required fields')
return
}
shippingAddress = formData
// Save address if user is authenticated and opted to save
if (status === 'authenticated' && saveAddress) {
try {
const addressData = {
firstName: formData.firstName,
lastName: formData.lastName,
address1: formData.address,
city: formData.city,
state: formData.state,
zipCode: formData.zipCode,
phone: formData.phone,
isDefault: addresses.length === 0, // Set as default if it's the first address
type: 'HOME'
}
const response = await fetch('/api/user/addresses', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(addressData)
})
if (response.ok) {
const newAddress = await response.json()
savedAddressId = newAddress.address.id
toast.success('Address saved for future use!')
}
} catch (error) {
console.error('Error saving address:', error)
// Don't fail the order if address saving fails
}
}
} else {
const selectedAddr = getSelectedAddress()
if (!selectedAddr) {
toast.error('Please select a shipping address')
return
}
// Check if email is provided
if (!formData.email) {
toast.error('Please provide your email address')
return
}
shippingAddress = {
email: formData.email,
firstName: selectedAddr.firstName,
lastName: selectedAddr.lastName,
address: `${selectedAddr.address1}${selectedAddr.address2 ? ', ' + selectedAddr.address2 : ''}`,
city: selectedAddr.city,
state: selectedAddr.state,
zipCode: selectedAddr.zipCode,
phone: selectedAddr.phone || formData.phone || ''
}
savedAddressId = selectedAddressId
}
setLoading(true)
try {
const orderData = {
items: cart.map(item => ({
productId: item.id,
quantity: item.quantity,
price: item.price
})),
total: getTotal(),
shippingAddress,
shippingAddressId: savedAddressId
}
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(orderData)
})
if (!response.ok) {
const errorData = await response.json()
if (response.status === 401) {
toast.error('Please sign in to place an order')
router.push('/auth/signin?callbackUrl=/checkout')
return
}
throw new Error(errorData.error || 'Failed to create order')
}
const order = await response.json()
// Initialize Razorpay payment
await initializePayment(order)
} catch (error) {
console.error('Error placing order:', error)
toast.error('Failed to place order. Please try again.')
} finally {
setLoading(false)
}
}
const initializePayment = async (order: any) => {
setProcessingPayment(true)
// Add debugging logs
console.log('Order data received:', order)
console.log('Razorpay Key ID:', order.razorpayKeyId)
console.log('Razorpay Order ID:', order.razorpayOrderId)
console.log('Total amount:', order.total)
console.log('Environment:', order.isProduction ? 'Production' : 'Test')
// Check if Razorpay is loaded
if (!window.Razorpay) {
toast.error('Payment gateway not loaded. Please refresh the page.')
setProcessingPayment(false)
return
}
// Validate required data
if (!order.razorpayKeyId) {
toast.error('Payment configuration error. Please try again.')
setProcessingPayment(false)
return
}
// Show environment warning for test mode
if (!order.isProduction) {
toast.info('Running in test mode - No actual payment will be charged')
}
const options = {
key: order.razorpayKeyId,
amount: Math.round(order.total * 100), // Convert to paise
currency: 'INR',
name: 'Padmaaja Rasooi',
description: `Order Payment ${order.isProduction ? '' : '(Test Mode)'}`,
order_id: order.razorpayOrderId,
handler: async function (response: any) {
try {
console.log('Payment response:', response)
const verifyResponse = await fetch('/api/orders/verify-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
razorpay_order_id: response.razorpay_order_id,
razorpay_payment_id: response.razorpay_payment_id,
razorpay_signature: response.razorpay_signature,
orderId: order.id
})
})
if (verifyResponse.ok) {
cartManager.clearCart()
toast.success('Payment successful! Order confirmed.')
router.push(`/order-confirmation?orderId=${order.id}`)
} else {
const errorData = await verifyResponse.json()
console.error('Verification failed:', errorData)
toast.error('Payment verification failed')
}
} catch (error) {
console.error('Payment verification error:', error)
toast.error('Payment verification failed')
} finally {
setProcessingPayment(false)
}
},
prefill: {
name: formData.firstName + ' ' + formData.lastName,
email: formData.email,
contact: formData.phone
},
modal: {
ondismiss: function() {
setProcessingPayment(false)
toast.error('Payment cancelled')
}
},
theme: {
color: order.isProduction ? '#10B981' : '#F59E0B' // Green for production, amber for test
}
}
console.log('Razorpay options:', options)
try {
const rzp = new window.Razorpay(options)
rzp.open()
} catch (error) {
console.error('Error opening Razorpay:', error)
toast.error('Failed to open payment gateway')
setProcessingPayment(false)
}
}
// Load Razorpay script
useEffect(() => {
const script = document.createElement('script')
script.src = 'https://checkout.razorpay.com/v1/checkout.js'
script.async = true
script.onload = () => {
console.log('Razorpay script loaded successfully')
}
script.onerror = () => {
console.error('Failed to load Razorpay script')
toast.error('Failed to load payment gateway')
}
document.body.appendChild(script)
return () => {
if (document.body.contains(script)) {
document.body.removeChild(script)
}
}
}, [])
if (cart.length === 0) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4">Your cart is empty</h1>
<Button asChild>
<Link href="/products">Continue Shopping</Link>
</Button>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<Button variant="ghost" asChild className="mb-4">
<Link href="/products">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Products
</Link>
</Button>
<h1 className="text-3xl font-bold text-gray-900">Checkout</h1>
{status === 'unauthenticated' && (
<p className="text-gray-600 mt-2">
<Link href="/auth/signin?callbackUrl=/checkout" className="text-blue-600 hover:underline">
Sign in
</Link> for a faster checkout experience
</p>
)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Checkout Form */}
<div className="space-y-6">
{/* Contact Information */}
<Card>
<CardHeader>
<CardTitle>Contact Information</CardTitle>
{status === 'authenticated' && (
<CardDescription>
Welcome back, {session?.user?.name || session?.user?.email}!
</CardDescription>
)}
</CardHeader>
<CardContent>
<div>
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
placeholder="your@email.com"
disabled={status === 'authenticated' && !!session?.user?.email}
required
/>
{status === 'authenticated' && session?.user?.email && (
<p className="text-xs text-gray-500 mt-1">
Using your account email
</p>
)}
</div>
</CardContent>
</Card>
{/* Shipping Address */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<Truck className="h-5 w-5 mr-2" />
Shipping Address
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{status === 'authenticated' && addresses.length > 0 && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label>Choose saved address</Label>
<Button
variant="outline"
size="sm"
onClick={() => setUseNewAddress(!useNewAddress)}
>
{useNewAddress ? 'Use Saved Address' : 'Use New Address'}
</Button>
</div>
{!useNewAddress && (
<div className="space-y-3">
{addresses.map((address) => (
<div
key={address.id}
className={`border rounded-lg p-4 cursor-pointer transition-colors ${
selectedAddressId === address.id
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => setSelectedAddressId(address.id)}
>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center space-x-2 mb-1">
<span className="font-medium">
{address.firstName} {address.lastName}
</span>
{address.isDefault && (
<Badge variant="secondary" className="text-xs">Default</Badge>
)}
<Badge variant="outline" className="text-xs">
{address.type}
</Badge>
</div>
<div className="text-sm text-gray-600">
{address.company && <div>{address.company}</div>}
<div>{address.address1}</div>
{address.address2 && <div>{address.address2}</div>}
<div>{address.city}, {address.state} {address.zipCode}</div>
{address.phone && <div>{address.phone}</div>}
</div>
</div>
<input
type="radio"
checked={selectedAddressId === address.id}
onChange={() => setSelectedAddressId(address.id)}
className="mt-1"
/>
</div>
</div>
))}
<Button
variant="outline"
className="w-full"
asChild
>
<Link href="/profile/addresses">
<MapPin className="h-4 w-4 mr-2" />
Manage Addresses
</Link>
</Button>
</div>
)}
</div>
)}
{(useNewAddress || addresses.length === 0 || status === 'unauthenticated') && (
<div className="space-y-4">
{status === 'unauthenticated' && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 mb-4">
<p className="text-sm text-blue-800">
💡 <Link href="/auth/signin?callbackUrl=/checkout" className="font-medium underline">Sign in</Link> to use saved addresses and speed up checkout
</p>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="firstName">First Name</Label>
<Input
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleInputChange}
required
/>
</div>
<div>
<Label htmlFor="lastName">Last Name</Label>
<Input
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleInputChange}
required
/>
</div>
</div>
<div>
<Label htmlFor="phone">Phone</Label>
<Input
id="phone"
name="phone"
value={formData.phone}
onChange={handleInputChange}
required
/>
</div>
<div>
<Label htmlFor="address">Address</Label>
<Input
id="address"
name="address"
value={formData.address}
onChange={handleInputChange}
placeholder="Street address"
required
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<Label htmlFor="city">City</Label>
<Input
id="city"
name="city"
value={formData.city}
onChange={handleInputChange}
required
/>
</div>
<div>
<Label htmlFor="state">State</Label>
<Input
id="state"
name="state"
value={formData.state}
onChange={handleInputChange}
required
/>
</div>
<div>
<Label htmlFor="zipCode">ZIP Code</Label>
<Input
id="zipCode"
name="zipCode"
value={formData.zipCode}
onChange={handleInputChange}
required
/>
</div>
</div>
{/* Save Address Option for Authenticated Users */}
{status === 'authenticated' && (
<div className="flex items-center space-x-2 pt-4 border-t border-gray-200">
<input
type="checkbox"
id="saveAddress"
checked={saveAddress}
onChange={(e) => setSaveAddress(e.target.checked)}
className="rounded"
/>
<Label htmlFor="saveAddress" className="text-sm">
Save this address for future orders
</Label>
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* Payment Method */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<CreditCard className="h-5 w-5 mr-2" />
Payment Method
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<p className="text-gray-600">Payment will be processed securely via Razorpay</p>
{process.env.NODE_ENV === 'development' && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
<p className="text-sm text-amber-800">
🧪 <strong>Development Mode:</strong> Using Razorpay test environment - No real payments will be processed
</p>
</div>
)}
</div>
</CardContent>
</Card>
</div>
{/* Order Summary */}
<div>
<Card>
<CardHeader>
<CardTitle>Order Summary</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{cart.map((item) => (
<div key={item.id} className="flex items-center space-x-4">
<Image
src={item.image || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={item.name}
width={60}
height={60}
className="rounded-lg object-cover"
/>
<div className="flex-1">
<h4 className="font-medium">{item.name}</h4>
<p className="text-sm text-gray-500">Quantity: {item.quantity}</p>
</div>
<div className="text-right">
<p className="font-medium">
{(item.price * item.quantity).toFixed(2)}
</p>
</div>
</div>
))}
<Separator />
<div className="space-y-2">
<div className="flex justify-between">
<span>Subtotal</span>
<span>{getSubtotal().toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span>Shipping</span>
<span className="text-green-600">Free</span>
</div>
<Separator />
<div className="flex justify-between font-bold text-lg">
<span>Total</span>
<span>{getTotal().toFixed(2)}</span>
</div>
</div>
<Button
className="w-full"
size="lg"
onClick={handlePlaceOrder}
disabled={loading || processingPayment}
>
{loading ? 'Creating Order...' : processingPayment ? 'Processing Payment...' : 'Place Order & Pay'}
</Button>
</CardContent>
</Card>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,318 @@
'use client'
import { motion } from 'framer-motion'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
import { Badge } from '@/components/ui/badge'
import { MapPin, Phone, Mail, Clock, Send, ChevronRight } from 'lucide-react'
import PageHero from '@/components/sections/PageHero'
import ContactForm from '@/components/forms/ContactForm'
export default function ContactPage() {
const contactInfo = [
{
icon: MapPin,
title: 'Manufacturing Unit',
details: 'Morabba No. 03, Firozpur Road',
subtitle: 'Kaithal Siwan, Haryana',
highlight: 'Production Facility'
},
{
icon: MapPin,
title: 'Corporate Office',
details: '11B/79 Vrindavan Yojna',
subtitle: 'Raibareli Road, Lucknow, India',
highlight: 'Business Operations'
}
]
const businessInfo = [
{
icon: Phone,
title: 'Business Phone',
details: '+91 94757 58817',
subtitle: 'Direct line to our sales team',
highlight: 'Mon-Sat 9AM-6PM IST'
},
{
icon: Mail,
title: 'Business Email',
details: 'info@padmajarice.com',
subtitle: 'For inquiries and partnerships',
highlight: 'Quick Response'
},
{
icon: Clock,
title: 'Business Hours',
details: 'Always Open',
subtitle: '24 x 7 x 375',
highlight: 'Always Welcome'
}
]
return (
<div className="min-h-screen">
{/* Hero Section */}
<PageHero
title=""
subtitle="Contact Us"
description="Partner with India's premium Basmati rice manufacturer. Connect with us for wholesale opportunities, bulk orders, and business partnerships."
badge={{
text: "Professional • Reliable • Trusted"
}}
backgroundGradient="from-blue-600/10 to-emerald-600/10"
titleGradient="from-blue-600 to-emerald-600"
/>
{/* Main Content */}
<div className="bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
{/* Professional Contact Info Cards */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="mb-20"
>
{/* Addresses Section */}
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900 mb-4">Our Locations</h2>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
Visit our manufacturing facility and corporate office for business partnerships and quality assurance.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-16">
{contactInfo.map((info, index) => (
<motion.div
key={info.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 + index * 0.1 }}
whileHover={{ y: -8 }}
className="group"
>
<Card className="h-full hover:shadow-2xl transition-all duration-300 border-0 shadow-lg bg-gradient-to-br from-white to-gray-50 group-hover:from-blue-50 group-hover:to-white">
<CardContent className="p-8 text-center">
<div className="w-20 h-20 bg-gradient-to-br from-blue-100 to-blue-200 rounded-2xl flex items-center justify-center mx-auto mb-6 group-hover:from-blue-500 group-hover:to-blue-600 transition-all duration-300">
<info.icon className="h-10 w-10 text-blue-600 group-hover:text-white transition-colors duration-300" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">
{info.title}
</h3>
<p className="text-gray-900 font-semibold mb-2 text-lg">
{info.details}
</p>
<p className="text-sm text-gray-600 mb-2">
{info.subtitle}
</p>
<div className="mt-3 pt-3 border-t border-gray-200">
<Badge variant="outline" className="text-xs font-medium bg-blue-50 text-blue-700 border-blue-200">
{info.highlight}
</Badge>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
{/* Business Contact Section */}
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900 mb-4">Business Contact</h2>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
Get in touch with our professional team for business inquiries and partnerships.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{businessInfo.map((info, index) => (
<motion.div
key={info.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 + index * 0.1 }}
whileHover={{ y: -8 }}
className="group"
>
<Card className="h-full hover:shadow-2xl transition-all duration-300 border-0 shadow-lg bg-gradient-to-br from-white to-gray-50 group-hover:from-emerald-50 group-hover:to-white">
<CardContent className="p-8 text-center">
<div className="w-20 h-20 bg-gradient-to-br from-emerald-100 to-emerald-200 rounded-2xl flex items-center justify-center mx-auto mb-6 group-hover:from-emerald-500 group-hover:to-emerald-600 transition-all duration-300">
<info.icon className="h-10 w-10 text-emerald-600 group-hover:text-white transition-colors duration-300" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">
{info.title}
</h3>
<p className="text-gray-900 font-semibold mb-2 text-lg">
{info.details}
</p>
<p className="text-sm text-gray-600 mb-2">
{info.subtitle}
</p>
<div className="mt-3 pt-3 border-t border-gray-200">
<Badge variant="outline" className="text-xs font-medium bg-emerald-50 text-emerald-700 border-emerald-200">
{info.highlight}
</Badge>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
</motion.div>
{/* Business Partnership CTA */}
{/* <motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="mb-16 bg-gradient-to-r from-blue-600 to-blue-700 rounded-2xl p-8 md:p-12 text-white"
>
<div className="text-center">
<h3 className="text-2xl md:text-3xl font-bold mb-4">Ready to Partner with Us?</h3>
<p className="text-blue-100 text-lg mb-6 max-w-3xl mx-auto">
Join India's leading Basmati rice manufacturer. We offer competitive wholesale pricing,
premium quality assurance, and reliable supply chain solutions for your business.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
<div className="text-center">
<div className="text-3xl font-bold text-blue-100">200+ CR</div>
<div className="text-blue-200 text-sm">Annual Revenue</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-blue-100">FSSAI</div>
<div className="text-blue-200 text-sm">Certified Quality</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-blue-100">24/7</div>
<div className="text-blue-200 text-sm">Quality Focus</div>
</div>
</div>
</div>
</motion.div> */}
{/* Professional Contact Form & Information */}
<section className="py-20 bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-gray-900 mb-4">Contact Us</h2>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
Ready to partner with us? Get in touch with our team for business inquiries and partnerships.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Modern Minimal Contact Form */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="order-2 lg:order-1"
>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm hover:shadow-md transition-shadow duration-300">
<div className="p-8">
<div className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-2">Get in touch</h2>
<p className="text-gray-600">
Connect with our team for business partnerships and bulk orders.
</p>
</div>
<ContactForm />
</div>
</div>
</motion.div>
{/* Business Information - Clean Masonry Layout */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="order-1 lg:order-2"
>
{/* Masonry Grid Layout */}
<div className="grid grid-cols-1 gap-6 auto-rows-max">
{/* Google Maps - Simplified */}
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
<div className="p-4 border-b border-gray-100">
<div className="flex items-center text-gray-900 font-medium">
<MapPin className="h-4 w-4 text-orange-600 mr-2" />
Office Location
</div>
</div>
<div className="relative h-48 w-full">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3559.6756662889756!2d80.94482677549088!3d26.780273676736253!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x399bfb07d4460727%3A0xdd3f66282f88d3ce!2s11-B%2F79%2C%20Brij%20Vihar%2C%20Sector%2011%2C%20Vrindavan%20Colony%2C%20Lucknow%2C%20Uttar%20Pradesh%20226029!5e0!3m2!1sen!2sin!4v1697000000000!5m2!1sen!2sin"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen={true}
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
></iframe>
</div>
<div className="p-4 bg-gray-50">
<p className="text-sm font-medium text-gray-900">11B/79</p>
<p className="text-sm text-gray-600">Vrindavan Yojna, Lucknow - 226029</p>
</div>
</div>
{/* Quick Stats - Minimal Grid */}
<div className="grid grid-cols-2 gap-4">
<div className="bg-orange-50 rounded-xl border border-orange-100 p-4 text-center">
<div className="text-2xl font-bold text-orange-600 mb-1">200+ CR</div>
<div className="text-xs text-gray-600">Annual Revenue</div>
</div>
<div className="bg-green-50 rounded-xl border border-green-100 p-4 text-center">
<div className="text-2xl font-bold text-green-600 mb-1">FSSAI</div>
<div className="text-xs text-gray-600">Certified</div>
</div>
</div>
{/* FAQ - Minimal Accordion */}
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
<h3 className="font-semibold text-gray-900 mb-4">Quick FAQ</h3>
<Accordion type="single" collapsible className="w-full space-y-2">
<AccordionItem value="item-1" className="border-b border-gray-100 pb-2">
<AccordionTrigger className="text-left text-sm font-medium py-2 hover:no-underline">
Minimum order quantity?
</AccordionTrigger>
<AccordionContent className="text-sm text-gray-600 pb-2">
1000 kg minimum for premium Basmati rice.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2" className="border-b border-gray-100 pb-2">
<AccordionTrigger className="text-left text-sm font-medium py-2 hover:no-underline">
Do you provide samples?
</AccordionTrigger>
<AccordionContent className="text-sm text-gray-600 pb-2">
Yes, free samples up to 1 kg for evaluation.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3" className="border-none">
<AccordionTrigger className="text-left text-sm font-medium py-2 hover:no-underline">
Payment terms?
</AccordionTrigger>
<AccordionContent className="text-sm text-gray-600">
Flexible terms with advance discounts available.
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
</motion.div>
</div>
</div>
</section>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,273 @@
import { Metadata } from 'next'
import { prisma } from '@/lib/prisma'
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { ArrowRight, Star, Award, ShieldCheck, Wheat } from 'lucide-react'
import ProductCard from '@/components/ProductCard'
import { MultipleStructuredData } from '@/components/StructuredData'
import { generateProductListJsonLd, generateBreadcrumbJsonLd, generateFAQJsonLd } from '@/lib/structured-data'
export const metadata: Metadata = {
title: 'Kashmina Rice - Premium Basmati Rice | Kashmina Steam & Sella | Padmaaja Rasooi',
description: 'Discover authentic Kashmina Rice - Premium basmati with extraordinary length, exquisite aroma and royal taste. Available in Kashmina Steam Grade-1, Kashmina Sella, Kashmina Dubar & Kashmina Tibar varieties. Export quality, lab tested, FSSAI approved.',
keywords: [
'kashmina rice',
'kashmina basmati rice',
'kashmina steam rice',
'kashmina sella rice',
'kashmina steam grade 1',
'kashmina sella tibar',
'kashmina dubar',
'premium kashmina rice',
'authentic kashmina basmati',
'kashmina rice online',
'kashmina rice price',
'padmaaja kashmina rice',
'buy kashmina rice',
'kashmina rice india',
'export quality kashmina rice'
],
openGraph: {
title: 'Kashmina Rice - Premium Basmati | Padmaaja Rasooi',
description: 'Authentic Kashmina Rice with extraordinary length, exquisite aroma and royal taste. Premium basmati varieties - Steam, Sella, Dubar & Tibar.',
type: 'website',
images: ['/images/kashmina-rice.jpg'],
},
alternates: {
canonical: '/kashmina-rice',
},
}
async function getKashminaProducts() {
try {
const products = await prisma.product.findMany({
where: {
isActive: true,
OR: [
{ name: { contains: 'Kashmina', mode: 'insensitive' } },
{ brand: { contains: 'Kashmina', mode: 'insensitive' } },
{ description: { contains: 'Kashmina', mode: 'insensitive' } }
]
},
include: {
category: true
},
orderBy: [
{ stock: 'desc' },
{ createdAt: 'desc' }
]
})
return products
} catch (error) {
console.error('Failed to fetch Kashmina products:', error)
return []
}
}
export default async function KashminaRicePage() {
const products = await getKashminaProducts()
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://padmaajarasooi.com'
// Generate JSON-LD for SEO
const productListJsonLd = generateProductListJsonLd(products, baseUrl)
const breadcrumbJsonLd = generateBreadcrumbJsonLd([
{ name: 'Home', url: '/' },
{ name: 'Kashmina Rice', url: '/kashmina-rice' }
], baseUrl)
const faqJsonLd = generateFAQJsonLd([
{
question: 'What is Kashmina Rice?',
answer: 'Kashmina Rice is a premium basmati rice variety known for its extraordinary grain length, exquisite aroma, and royal taste. It is aged basmati rice that undergoes strict quality control and is available in various grades including Steam and Sella processing.'
},
{
question: 'What are the different types of Kashmina Rice available?',
answer: 'Kashmina Rice is available in multiple varieties: Kashmina Steam Grade-1, Kashmina Steam Dubar, Kashmina Steam Tibar, Kashmina Sella Grade-1, Kashmina Sella Tibar, and Kashmina Sella Dubar. Each variety offers unique cooking properties and taste profiles.'
},
{
question: 'Is Kashmina Rice export quality?',
answer: 'Yes, Kashmina Rice meets international quality standards and is export-grade basmati rice. It is lab-tested, FSSAI approved, and certified for quality and purity.'
},
{
question: 'What is the difference between Kashmina Steam and Kashmina Sella?',
answer: 'Kashmina Steam rice is processed with steam for enhanced aroma and taste, resulting in softer grains. Kashmina Sella undergoes parboiling before milling, making it firmer with better texture, ideal for biryani and special occasions.'
},
{
question: 'Where can I buy Kashmina Rice?',
answer: 'You can buy authentic Kashmina Rice from Padmaaja Rasooi online. We offer various pack sizes with home delivery across India.'
}
])
return (
<>
{/* SEO: Structured Data */}
<MultipleStructuredData
dataArray={[productListJsonLd, breadcrumbJsonLd, faqJsonLd]}
idPrefix="kashmina"
/>
<div className="min-h-screen bg-gradient-to-br from-amber-50 via-yellow-50 to-orange-50">
{/* Hero Section */}
<section className="relative py-20 overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_20%,rgba(245,158,11,0.1),transparent_50%)]"></div>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_70%_80%,rgba(251,191,36,0.1),transparent_50%)]"></div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Breadcrumb */}
<nav className="mb-8 text-sm">
<ol className="flex items-center space-x-2 text-gray-600">
<li><Link href="/" className="hover:text-amber-600">Home</Link></li>
<li>/</li>
<li className="text-amber-600 font-semibold">Kashmina Rice</li>
</ol>
</nav>
<div className="text-center max-w-4xl mx-auto">
<Badge className="mb-6 bg-gradient-to-r from-amber-500 to-orange-500 text-white px-6 py-2">
Premium Basmati Rice
</Badge>
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold text-gray-900 mb-6">
<span className="text-transparent bg-gradient-to-r from-amber-600 via-yellow-600 to-orange-600 bg-clip-text">
Kashmina Rice
</span>
</h1>
<p className="text-xl md:text-2xl text-gray-700 mb-8 leading-relaxed">
Authentic aged Basmati rice with <span className="font-bold text-amber-600">extraordinary length</span>,
<span className="font-bold text-amber-600"> exquisite aroma</span>, and
<span className="font-bold text-amber-600"> royal taste</span>
</p>
{/* Key Features */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-12">
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-6 shadow-lg">
<Wheat className="h-8 w-8 text-emerald-600 mx-auto mb-2" />
<p className="font-semibold text-gray-900">Authentic Grains</p>
</div>
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-6 shadow-lg">
<Award className="h-8 w-8 text-blue-600 mx-auto mb-2" />
<p className="font-semibold text-gray-900">Export Quality</p>
</div>
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-6 shadow-lg">
<ShieldCheck className="h-8 w-8 text-purple-600 mx-auto mb-2" />
<p className="font-semibold text-gray-900">Lab Tested</p>
</div>
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-6 shadow-lg">
<Star className="h-8 w-8 text-amber-600 mx-auto mb-2" />
<p className="font-semibold text-gray-900">Premium Grade</p>
</div>
</div>
</div>
</div>
</section>
{/* Products Section */}
<section className="py-16 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
Kashmina Rice Varieties
</h2>
<p className="text-lg text-gray-600 max-w-3xl mx-auto">
Choose from our premium collection of Kashmina Steam and Sella rice in various grades
</p>
</div>
{products.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{products.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
))}
</div>
) : (
<div className="text-center py-12">
<p className="text-gray-600 text-lg mb-6">
Kashmina Rice products are currently being updated. Please check back soon!
</p>
<Link href="/products">
<Button size="lg">
View All Products
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</Link>
</div>
)}
</div>
</section>
{/* About Kashmina Section */}
<section className="py-16 bg-gradient-to-br from-amber-50 to-orange-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-8 text-center">
Why Choose Kashmina Rice?
</h2>
<div className="space-y-6 bg-white/80 backdrop-blur-sm rounded-2xl p-8 shadow-lg">
<div>
<h3 className="text-xl font-bold text-gray-900 mb-3">Premium Quality</h3>
<p className="text-gray-700 leading-relaxed">
Kashmina Rice is a treasured grain variety known for its distinct aroma and great nutty taste.
Each grain is carefully selected and aged to perfection, ensuring the highest quality standards.
</p>
</div>
<div>
<h3 className="text-xl font-bold text-gray-900 mb-3">Export Grade Standards</h3>
<p className="text-gray-700 leading-relaxed">
Our Kashmina Rice meets international quality standards, making it suitable for global markets.
It undergoes rigorous quality checks and lab testing to ensure purity and excellence.
</p>
</div>
<div>
<h3 className="text-xl font-bold text-gray-900 mb-3">Multiple Varieties</h3>
<p className="text-gray-700 leading-relaxed">
Available in both Steam and Sella processing, with various grades including Grade-1, Dubar, and Tibar.
Whether you need rice for daily cooking or special occasions, we have the perfect variety for you.
</p>
</div>
<div>
<h3 className="text-xl font-bold text-gray-900 mb-3">Certified & Safe</h3>
<p className="text-gray-700 leading-relaxed">
FSSAI approved and certified for quality. Our Kashmina Rice is free from harmful chemicals,
pesticide-free, and processed in hygienic conditions to ensure your family's health and safety.
</p>
</div>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-16 bg-gradient-to-r from-amber-600 to-orange-600">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl md:text-4xl font-bold text-white mb-6">
Experience the Royal Taste of Kashmina Rice
</h2>
<p className="text-xl text-white/90 mb-8">
Order now and enjoy premium quality basmati rice delivered to your doorstep
</p>
<div className="flex flex-wrap justify-center gap-4">
<Link href="/products">
<Button size="lg" variant="secondary">
Shop All Products
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</Link>
<Link href="/contact">
<Button size="lg" variant="outline" className="bg-white/10 text-white border-white hover:bg-white hover:text-amber-600">
Contact Us
</Button>
</Link>
</div>
</div>
</section>
</div>
</>
)
}

38
app/(public)/layout.tsx Normal file
View File

@@ -0,0 +1,38 @@
import type { Metadata } from 'next'
import { Header } from '@/components/layout/header'
import { Footer } from '@/components/layout/footer'
import MobileTabBar from '@/components/layout/MobileTabBar'
import CTA from '@/components/sections/cta'
import UpdateNotification from '@/components/UpdateNotification'
export const metadata: Metadata = {
title: 'Padmaaja Rasooi - Premium Food Products | Multigrain Flour & Rice',
description: 'Premium quality multigrain flour and rice products. ISO certified food processing company specializing in SHREE AAHAR multigrain flour, basmati and non-basmati rice.'
}
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="min-h-screen flex flex-col">
<Header />
<div className="flex-1 pb-16 md:pb-0">
{children}
</div>
<CTA />
<Footer />
{/* PWA Update Notification */}
<UpdateNotification />
{/* Mobile Tab Bar */}
<MobileTabBar />
{/* Development Cache Manager */}
{/* <DevCacheManager /> */}
</div>
)
}

85
app/(public)/loading.tsx Normal file
View File

@@ -0,0 +1,85 @@
'use client'
import { motion } from 'framer-motion'
import { Package, Sparkles } from 'lucide-react'
export default function PublicLoading() {
return (
<div className="min-h-[60vh] flex items-center justify-center px-4">
<div className="text-center">
{/* Compact Loading Animation */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
className="mb-6"
>
<div className="relative mx-auto w-20 h-20 mb-4">
{/* Spinner */}
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 1.5,
repeat: Infinity,
ease: "linear"
}}
className="absolute inset-0 border-3 border-emerald-200 border-t-emerald-600 rounded-full"
/>
{/* Center icon */}
<motion.div
animate={{
scale: [1, 1.1, 1]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
className="absolute inset-2 flex items-center justify-center bg-white rounded-full shadow-sm"
>
<Package className="w-8 h-8 text-emerald-600" />
</motion.div>
</div>
</motion.div>
{/* Loading Text */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<div className="flex items-center justify-center space-x-2 text-emerald-600 font-medium">
<Sparkles className="w-4 h-4" />
<span>Loading...</span>
</div>
</motion.div>
{/* Subtle animated dots */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="flex justify-center space-x-1 mt-3"
>
{[0, 1, 2].map((index) => (
<motion.div
key={index}
className="w-2 h-2 bg-emerald-400 rounded-full"
animate={{
scale: [1, 1.3, 1],
opacity: [0.4, 1, 0.4]
}}
transition={{
duration: 1.2,
repeat: Infinity,
delay: index * 0.15,
ease: "easeInOut"
}}
/>
))}
</motion.div>
</div>
</div>
)
}

View File

@@ -0,0 +1,26 @@
import { LazyNewsSection } from '@/components/LazyComponents'
import PageHero from '@/components/sections/PageHero'
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'News & Updates | Padmaaja Rasooi',
description: 'Stay updated with latest news, achievements, and developments from Padmaaja Rasooi. Industry insights, company milestones, and market updates.',
keywords: 'news, updates, achievements, press releases, industry news, company news, rice industry, export news',
}
export default function NewsPage() {
return (
<div className="min-h-screen bg-white">
<PageHero
title="News & Updates"
subtitle="Latest Developments"
description="Stay informed about our latest achievements, innovations, industry developments, and company milestones."
badge={{ text: "Media Center" }}
backgroundGradient="from-blue-600/10 to-purple-600/10"
titleGradient="from-blue-600 to-purple-600"
/>
<LazyNewsSection />
</div>
)
}

View File

@@ -0,0 +1,563 @@
'use client'
import { useEffect, useState, useCallback } from 'react'
import { useSearchParams, useRouter } from 'next/navigation'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { CheckCircle, Package, Truck, Clock, ArrowRight, Star, Gift, Heart } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
import Link from 'next/link'
import Image from 'next/image'
import { toast } from 'sonner'
import ConfettiBoom from 'react-confetti-boom'
interface OrderConfirmation {
orderId: string
total: number
status: string
estimatedDelivery: string
createdAt: string
items: {
id: string
name: string
quantity: number
price: number
image: string
discount: number
}[]
customer: {
name: string
email: string
}
shippingAddress?: {
name: string
address: string
address2?: string
city: string
state: string
zipCode: string
phone?: string
}
}
export default function OrderConfirmationPage() {
const searchParams = useSearchParams()
const router = useRouter()
const [orderData, setOrderData] = useState<OrderConfirmation | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [showConfetti, setShowConfetti] = useState(false)
const fetchOrderData = useCallback(async (orderId: string) => {
try {
setLoading(true)
setError(null)
const response = await fetch(`/api/order-confirmation/${orderId}`)
if (!response.ok) {
if (response.status === 401) {
router.push('/auth/signin?callbackUrl=/order-confirmation?orderId=' + orderId)
return
}
throw new Error('Order not found')
}
const data = await response.json()
setOrderData(data)
} catch (error) {
console.error('Error fetching order:', error)
setError('Failed to load order details')
} finally {
setLoading(false)
}
}, [router])
useEffect(() => {
const orderId = searchParams.get('orderId')
if (!orderId) {
router.push('/products')
return
}
fetchOrderData(orderId)
}, [searchParams, router, fetchOrderData])
useEffect(() => {
if (orderData) {
// Trigger confetti after data loads
setShowConfetti(true)
// Show success toast
toast.success('🎉 Order placed successfully!')
// Stop confetti after 4 seconds
const timer = setTimeout(() => {
setShowConfetti(false)
}, 4000)
return () => clearTimeout(timer)
}
}, [orderData])
const getStatusColor = (status: string) => {
switch (status) {
case 'PENDING': return 'bg-amber-500'
case 'PAID': return 'bg-emerald-500'
case 'SHIPPED': return 'bg-emerald-600'
case 'DELIVERED': return 'bg-green-500'
case 'CANCELLED': return 'bg-red-500'
default: return 'bg-gray-500'
}
}
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-emerald-50 via-white to-green-50 pt-20">
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header Skeleton */}
<div className="text-center mb-10">
<div className="relative inline-block mb-6">
<div className="w-20 h-20 bg-gray-200 rounded-full animate-pulse mx-auto"></div>
</div>
<div className="h-10 bg-gray-200 rounded-lg mb-3 max-w-md mx-auto animate-pulse"></div>
<div className="h-6 bg-gray-200 rounded-lg mb-6 max-w-lg mx-auto animate-pulse"></div>
<div className="h-12 bg-gray-200 rounded-xl max-w-xs mx-auto animate-pulse"></div>
</div>
{/* Content Skeleton */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Content Skeleton - 2 columns */}
<div className="lg:col-span-2 space-y-6">
{/* Order Summary Card Skeleton */}
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
<div className="h-16 bg-gray-200 animate-pulse"></div>
<div className="p-6">
{/* Order header info skeleton */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6 p-4 bg-gray-50 rounded-lg">
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded animate-pulse"></div>
<div className="h-6 bg-gray-200 rounded animate-pulse"></div>
</div>
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded animate-pulse"></div>
<div className="h-6 bg-gray-200 rounded animate-pulse w-20 mx-auto"></div>
</div>
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded animate-pulse"></div>
<div className="h-8 bg-gray-200 rounded animate-pulse"></div>
</div>
</div>
{/* Items list skeleton */}
<div className="space-y-4">
<div className="h-6 bg-gray-200 rounded animate-pulse w-32"></div>
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center space-x-4 p-4 bg-gray-50 rounded-lg">
<div className="w-16 h-16 bg-gray-200 rounded-lg animate-pulse flex-shrink-0"></div>
<div className="flex-1 space-y-2">
<div className="h-5 bg-gray-200 rounded animate-pulse"></div>
<div className="flex space-x-4">
<div className="h-4 bg-gray-200 rounded animate-pulse w-16"></div>
<div className="h-4 bg-gray-200 rounded animate-pulse w-20"></div>
</div>
</div>
<div className="h-6 bg-gray-200 rounded animate-pulse w-20"></div>
</div>
))}
</div>
</div>
</div>
{/* Shipping Address Card Skeleton */}
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
<div className="h-16 bg-gray-200 animate-pulse"></div>
<div className="p-6 space-y-3">
<div className="h-5 bg-gray-200 rounded animate-pulse"></div>
<div className="h-4 bg-gray-200 rounded animate-pulse"></div>
<div className="h-4 bg-gray-200 rounded animate-pulse w-3/4"></div>
<div className="h-4 bg-gray-200 rounded animate-pulse w-1/2"></div>
</div>
</div>
</div>
{/* Sidebar Skeleton */}
<div className="space-y-6">
{/* Status Card Skeleton */}
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
<div className="h-16 bg-gray-200 animate-pulse"></div>
<div className="p-6 space-y-4">
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded animate-pulse w-20"></div>
<div className="h-5 bg-gray-200 rounded animate-pulse"></div>
<div className="h-4 bg-gray-200 rounded animate-pulse w-3/4"></div>
</div>
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded animate-pulse w-32"></div>
<div className="h-5 bg-gray-200 rounded animate-pulse"></div>
</div>
</div>
</div>
{/* Progress Card Skeleton */}
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
<div className="h-16 bg-gray-200 animate-pulse"></div>
<div className="p-6 space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gray-200 rounded-full animate-pulse"></div>
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-200 rounded animate-pulse"></div>
<div className="h-3 bg-gray-200 rounded animate-pulse w-3/4"></div>
</div>
</div>
))}
</div>
</div>
{/* Action Buttons Skeleton */}
<div className="space-y-3">
<div className="h-12 bg-gray-200 rounded-lg animate-pulse"></div>
<div className="h-12 bg-gray-200 rounded-lg animate-pulse"></div>
</div>
</div>
</div>
</div>
</div>
)
}
if (error || !orderData) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-gray-50 flex items-center justify-center pt-20">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4">
{error || 'Order not found'}
</h1>
<Button asChild>
<Link href="/products">Continue Shopping</Link>
</Button>
</div>
</div>
)
}
return (
<>
{/* Confetti Animation with highest z-index */}
{showConfetti && (
<div className="fixed inset-0 pointer-events-none z-[9999]">
<ConfettiBoom
particleCount={150}
effectCount={3}
colors={['#10B981', '#059669', '#34D399', '#6EE7B7', '#A7F3D0', '#D1FAE5']}
shapeSize={12}
spreadDeg={60}
effectInterval={300}
/>
</div>
)}
<div className="min-h-screen bg-gradient-to-br from-emerald-50 via-white to-green-50 pt-20">
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Professional Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-center mb-10"
>
<div className="relative inline-block mb-6">
<div className="absolute inset-0 bg-emerald-500 rounded-full blur-lg opacity-30 animate-pulse"></div>
<div className="relative w-20 h-20 bg-white rounded-full flex items-center justify-center shadow-xl border-4 border-emerald-100">
<CheckCircle className="h-10 w-10 text-emerald-500" />
</div>
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-3">
Order Confirmed!
</h1>
<p className="text-lg text-gray-600 max-w-lg mx-auto mb-6">
Thank you <span className="font-semibold text-emerald-600">{orderData.customer.name}</span>!
Your order has been confirmed and is being prepared.
</p>
<div className="inline-flex items-center bg-emerald-50 text-emerald-700 px-6 py-3 rounded-xl border border-emerald-200 shadow-sm">
<Package className="h-5 w-5 mr-2" />
<span className="font-semibold">Order #{orderData.orderId.slice(-8).toUpperCase()}</span>
</div>
{/* Floating Celebration Elements */}
<div className="absolute inset-0 pointer-events-none overflow-hidden">
<motion.div
animate={{
y: [0, -20, 0],
rotate: [0, 5, -5, 0],
scale: [1, 1.1, 1]
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut"
}}
className="absolute top-20 left-1/4 transform -translate-x-1/2"
>
<Star className="h-8 w-8 text-yellow-400 fill-current opacity-70" />
</motion.div>
<motion.div
animate={{
y: [0, -15, 0],
rotate: [0, -10, 10, 0],
scale: [1, 1.2, 1]
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
delay: 1
}}
className="absolute top-32 right-1/4 transform translate-x-1/2"
>
<Gift className="h-8 w-8 text-emerald-500 opacity-70" />
</motion.div>
<motion.div
animate={{
y: [0, -25, 0],
rotate: [0, 15, -15, 0],
scale: [1, 0.9, 1]
}}
transition={{
duration: 2.5,
repeat: Infinity,
ease: "easeInOut",
delay: 0.5
}}
className="absolute top-16 left-2/3"
>
<Heart className="h-6 w-6 text-red-500 fill-current opacity-70" />
</motion.div>
</div>
</motion.div>
{/* Professional Content Layout */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Order Information - Takes 2 columns */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="lg:col-span-2 space-y-6"
>
{/* Order Summary Card */}
<Card className="shadow-lg border-0 bg-white">
<CardHeader className="bg-gradient-to-r from-emerald-500 to-green-500 text-white">
<CardTitle className="flex items-center text-lg font-semibold">
<Package className="h-5 w-5 mr-3" />
Order Summary
</CardTitle>
</CardHeader>
<CardContent className="p-6">
{/* Order Header Info */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6 p-4 bg-gray-50 rounded-lg">
<div className="text-center md:text-left">
<div className="text-sm text-gray-500 mb-1">Order ID</div>
<div className="font-semibold text-gray-900">#{orderData.orderId.slice(-8).toUpperCase()}</div>
</div>
<div className="text-center">
<div className="text-sm text-gray-500 mb-1">Status</div>
<Badge className={`${getStatusColor(orderData.status)} text-white px-3 py-1 text-xs font-medium`}>
{orderData.status}
</Badge>
</div>
<div className="text-center md:text-right">
<div className="text-sm text-gray-500 mb-1">Total Amount</div>
<div className="text-xl font-bold text-emerald-600">{orderData.total.toFixed(2)}</div>
</div>
</div>
{/* Items List */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 border-b border-gray-200 pb-2">Items Ordered</h3>
{orderData.items.map((item, index) => {
const discountedPrice = item.discount > 0
? item.price - (item.price * item.discount / 100)
: item.price
return (
<motion.div
key={item.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.4, delay: 0.3 + (index * 0.1) }}
className="flex items-center space-x-4 p-4 bg-white border border-gray-100 rounded-lg hover:shadow-md transition-shadow"
>
<div className="relative flex-shrink-0">
<Image
src={item.image || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={item.name}
width={64}
height={64}
className="rounded-lg object-cover"
/>
{item.discount > 0 && (
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs px-2 py-1 rounded-full font-medium">
{item.discount}% OFF
</div>
)}
</div>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-gray-900 truncate">{item.name}</h4>
<div className="flex items-center space-x-4 mt-1">
<span className="text-sm text-gray-500">Qty: {item.quantity}</span>
<div className="flex items-center space-x-2">
<span className="font-semibold text-emerald-600">{discountedPrice.toFixed(2)}</span>
{item.discount > 0 && (
<span className="text-sm text-gray-400 line-through">{item.price.toFixed(2)}</span>
)}
</div>
</div>
</div>
<div className="text-right">
<div className="font-bold text-gray-900">{(discountedPrice * item.quantity).toFixed(2)}</div>
</div>
</motion.div>
)
})}
</div>
</CardContent>
</Card>
{/* Shipping Address Card */}
{orderData.shippingAddress && (
<Card className="shadow-lg border-0 bg-white">
<CardHeader className="bg-gradient-to-r from-emerald-500 to-green-500 text-white">
<CardTitle className="flex items-center text-lg font-semibold">
<Truck className="h-5 w-5 mr-3" />
Shipping Address
</CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="space-y-2 text-gray-700">
<p className="font-semibold text-gray-900">{orderData.shippingAddress.name}</p>
<p>{orderData.shippingAddress.address}</p>
{orderData.shippingAddress.address2 && (
<p>{orderData.shippingAddress.address2}</p>
)}
<p>{orderData.shippingAddress.city}, {orderData.shippingAddress.state} {orderData.shippingAddress.zipCode}</p>
{orderData.shippingAddress.phone && (
<p className="flex items-center mt-3">
<span className="text-gray-500 mr-2">Phone:</span>
{orderData.shippingAddress.phone}
</p>
)}
</div>
</CardContent>
</Card>
)}
</motion.div>
{/* Sidebar - Order Status & Actions */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="space-y-6"
>
{/* Customer & Status Card */}
<Card className="shadow-lg border-0 bg-white">
<CardHeader className="bg-gradient-to-r from-emerald-500 to-green-500 text-white">
<CardTitle className="flex items-center text-lg font-semibold">
<Clock className="h-5 w-5 mr-3" />
Order Status
</CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="space-y-4">
<div>
<div className="text-sm text-gray-500 mb-1">Customer</div>
<div className="font-medium text-gray-900">{orderData.customer.name}</div>
<div className="text-sm text-gray-600">{orderData.customer.email}</div>
</div>
<div>
<div className="text-sm text-gray-500 mb-1">Expected Delivery</div>
<div className="font-medium text-gray-900">
{new Date(orderData.estimatedDelivery).toLocaleDateString('en-US', {
weekday: 'long',
month: 'short',
day: 'numeric'
})}
</div>
</div>
</div>
</CardContent>
</Card>
{/* Progress Card */}
<Card className="shadow-lg border-0 bg-white">
<CardHeader className="bg-gradient-to-r from-emerald-500 to-green-500 text-white">
<CardTitle className="flex items-center text-lg font-semibold">
<ArrowRight className="h-5 w-5 mr-3" />
Order Progress
</CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="space-y-4">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-emerald-500 text-white rounded-full flex items-center justify-center text-sm font-bold">
1
</div>
<div>
<div className="font-medium text-gray-900">Order Processing</div>
<div className="text-sm text-gray-600">We're preparing your items</div>
</div>
</div>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gray-300 text-white rounded-full flex items-center justify-center text-sm font-bold">
2
</div>
<div>
<div className="font-medium text-gray-500">Shipped</div>
<div className="text-sm text-gray-500">You'll receive tracking information</div>
</div>
</div>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gray-300 text-white rounded-full flex items-center justify-center text-sm font-bold">
3
</div>
<div>
<div className="font-medium text-gray-500">Delivered</div>
<div className="text-sm text-gray-500">Your order arrives!</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Action Buttons */}
<div className="grid grid-cols-1 gap-2">
<Link href="/orders">
<Button className="w-full bg-emerald-600 hover:bg-emerald-700 text-white py-3 rounded-lg font-medium transition-colors">
View All Orders
</Button>
</Link>
<Link href="/products">
<Button variant="outline" className="w-full border-emerald-200 text-emerald-600 hover:bg-emerald-50 py-3 rounded-lg font-medium transition-colors">
Continue Shopping
</Button>
</Link>
</div>
</motion.div>
</div>
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,479 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { ArrowLeft, Package, Truck, CheckCircle, XCircle, Clock, Calendar, MapPin, Mail, CreditCard, Star } from 'lucide-react'
import { motion } from 'framer-motion'
import { toast } from 'sonner'
import Link from 'next/link'
import Image from 'next/image'
interface OrderDetails {
id: string
total: number
status: 'PENDING' | 'PAID' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED'
createdAt: string
updatedAt: string
razorpayOrderId: string | null
razorpayPaymentId: string | null
user: {
name: string
email: string
}
orderItems: {
id: string
quantity: number
price: number
product: {
id: string
name: string
images: string[]
discount: number
sku: string
}
}[]
}
const statusConfig = {
PENDING: {
label: 'Order Placed',
color: 'bg-gradient-to-r from-yellow-400 to-orange-500',
icon: Clock,
description: 'Your order is being processed'
},
PAID: {
label: 'Payment Confirmed',
color: 'bg-gradient-to-r from-blue-500 to-indigo-600',
icon: CreditCard,
description: 'Payment received successfully'
},
SHIPPED: {
label: 'Order Shipped',
color: 'bg-gradient-to-r from-purple-500 to-pink-600',
icon: Truck,
description: 'Your order is on the way'
},
DELIVERED: {
label: 'Delivered',
color: 'bg-gradient-to-r from-green-500 to-emerald-600',
icon: CheckCircle,
description: 'Order delivered successfully'
},
CANCELLED: {
label: 'Cancelled',
color: 'bg-gradient-to-r from-red-500 to-pink-600',
icon: XCircle,
description: 'Order was cancelled'
}
}
export default function OrderDetailsPage() {
const params = useParams()
const router = useRouter()
const [order, setOrder] = useState<OrderDetails | null>(null)
const [loading, setLoading] = useState(true)
const [cancelling, setCancelling] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetchOrder = useCallback(async () => {
if (!params.id) return
try {
setLoading(true)
setError(null)
const response = await fetch(`/api/orders/${params.id}`)
if (!response.ok) {
if (response.status === 401) {
router.push('/auth/signin?callbackUrl=/orders/' + params.id)
return
}
throw new Error('Order not found')
}
const data = await response.json()
setOrder(data)
} catch (error) {
console.error('Error fetching order:', error)
setError('Failed to load order details')
} finally {
setLoading(false)
}
}, [params.id, router])
useEffect(() => {
fetchOrder()
}, [fetchOrder])
const handleCancelOrder = async () => {
if (!order || !confirm('Are you sure you want to cancel this order?')) return
setCancelling(true)
try {
const response = await fetch(`/api/orders/${order.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'CANCELLED' })
})
if (!response.ok) throw new Error('Failed to cancel order')
toast.success('Order cancelled successfully')
fetchOrder()
} catch (error) {
toast.error('Failed to cancel order')
} finally {
setCancelling(false)
}
}
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
<div className="text-center">
<div className="relative">
<div className="w-20 h-20 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin mx-auto"></div>
<div className="w-12 h-12 border-4 border-gray-100 border-t-purple-500 rounded-full animate-spin absolute top-2 left-2 mx-auto"></div>
</div>
<p className="mt-4 text-gray-600 font-medium">Loading order details...</p>
</div>
</div>
)
}
if (error || !order) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center"
>
<div className="w-20 h-20 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-6">
<XCircle className="h-10 w-10 text-red-500" />
</div>
<h1 className="text-2xl font-bold text-gray-900 mb-4">
{error || 'Order not found'}
</h1>
<Button asChild className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700">
<Link href="/orders">Back to Orders</Link>
</Button>
</motion.div>
</div>
)
}
const StatusIcon = statusConfig[order.status].icon
const statusInfo = statusConfig[order.status]
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<Button variant="ghost" asChild className="mb-6 hover:bg-gray-100">
<Link href="/orders">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Orders
</Link>
</Button>
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-8">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6">
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-gray-900 to-gray-700 bg-clip-text text-transparent">
Order #{order.id.slice(-8)}
</h1>
<p className="text-gray-600 flex items-center mt-2">
<Calendar className="h-4 w-4 mr-2" />
Placed on {new Date(order.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
<div className="text-right">
<Badge className={`${statusInfo.color} text-white px-4 py-2 text-sm font-medium shadow-lg`}>
<StatusIcon className="h-4 w-4 mr-2" />
{statusInfo.label}
</Badge>
<p className="text-sm text-gray-500 mt-2">{statusInfo.description}</p>
</div>
</div>
</div>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Order Items */}
<div className="lg:col-span-2 space-y-6">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
>
<Card className="shadow-lg border-0 bg-white/80 backdrop-blur-sm">
<CardHeader className="pb-4">
<CardTitle className="flex items-center text-xl">
<Package className="h-6 w-6 mr-3 text-blue-600" />
Order Items
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{order.orderItems.map((item, index) => {
const discountedPrice = item.product.discount > 0
? item.price - (item.price * item.product.discount / 100)
: item.price
return (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 * index }}
className="group flex items-center space-x-4 p-6 border border-gray-100 rounded-xl hover:shadow-md transition-all duration-300 hover:border-gray-200 bg-gradient-to-r from-white to-gray-50"
>
<div className="relative">
<Image
src={item.product.images[0] || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={item.product.name}
width={80}
height={80}
className="rounded-xl object-cover shadow-md group-hover:shadow-lg transition-shadow"
/>
{item.product.discount > 0 && (
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs px-2 py-1 rounded-full">
{item.product.discount}% OFF
</div>
)}
</div>
<div className="flex-1">
<h3 className="font-semibold text-gray-900 group-hover:text-blue-600 transition-colors">
{item.product.name}
</h3>
<div className="flex items-center space-x-4 text-sm text-gray-500 mt-1">
<span>SKU: {item.product.sku}</span>
<span></span>
<span>Qty: {item.quantity}</span>
</div>
<div className="flex items-center mt-2">
{[...Array(5)].map((_, i) => (
<Star key={i} className="h-3 w-3 text-yellow-400 fill-current" />
))}
<span className="text-xs text-gray-500 ml-2">(4.5)</span>
</div>
</div>
<div className="text-right">
<p className="font-bold text-lg text-gray-900">
{(discountedPrice * item.quantity).toFixed(2)}
</p>
{item.product.discount > 0 && (
<p className="text-sm text-gray-400 line-through">
{(item.price * item.quantity).toFixed(2)}
</p>
)}
</div>
</motion.div>
)
})}
</div>
</CardContent>
</Card>
</motion.div>
{/* Order Timeline */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4 }}
>
<Card className="shadow-lg border-0 bg-white/80 backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-xl">Order Journey</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Order Placed */}
<div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-gradient-to-r from-green-400 to-green-600 rounded-full flex items-center justify-center shadow-lg">
<Calendar className="h-5 w-5 text-white" />
</div>
<div className="flex-1">
<p className="font-semibold text-gray-900">Order Placed</p>
<p className="text-sm text-gray-500">
{new Date(order.createdAt).toLocaleString()}
</p>
<div className="w-full bg-green-100 h-1 rounded-full mt-2"></div>
</div>
</div>
{/* Payment Confirmed */}
{['PAID', 'SHIPPED', 'DELIVERED'].includes(order.status) && (
<div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-gradient-to-r from-blue-400 to-blue-600 rounded-full flex items-center justify-center shadow-lg">
<CreditCard className="h-5 w-5 text-white" />
</div>
<div className="flex-1">
<p className="font-semibold text-gray-900">Payment Confirmed</p>
<p className="text-sm text-gray-500">Payment processed successfully</p>
<div className="w-full bg-blue-100 h-1 rounded-full mt-2"></div>
</div>
</div>
)}
{/* Order Shipped */}
{['SHIPPED', 'DELIVERED'].includes(order.status) && (
<div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-gradient-to-r from-purple-400 to-purple-600 rounded-full flex items-center justify-center shadow-lg">
<Truck className="h-5 w-5 text-white" />
</div>
<div className="flex-1">
<p className="font-semibold text-gray-900">Order Shipped</p>
<p className="text-sm text-gray-500">Your package is on the way</p>
<div className="w-full bg-purple-100 h-1 rounded-full mt-2"></div>
</div>
</div>
)}
{/* Order Delivered */}
{order.status === 'DELIVERED' && (
<div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-gradient-to-r from-green-400 to-green-600 rounded-full flex items-center justify-center shadow-lg">
<CheckCircle className="h-5 w-5 text-white" />
</div>
<div className="flex-1">
<p className="font-semibold text-gray-900">Order Delivered</p>
<p className="text-sm text-gray-500">Package delivered successfully</p>
<div className="w-full bg-green-100 h-1 rounded-full mt-2"></div>
</div>
</div>
)}
{/* Order Cancelled */}
{order.status === 'CANCELLED' && (
<div className="flex items-start space-x-4">
<div className="w-10 h-10 bg-gradient-to-r from-red-400 to-red-600 rounded-full flex items-center justify-center shadow-lg">
<XCircle className="h-5 w-5 text-white" />
</div>
<div className="flex-1">
<p className="font-semibold text-gray-900">Order Cancelled</p>
<p className="text-sm text-gray-500">Order was cancelled</p>
<div className="w-full bg-red-100 h-1 rounded-full mt-2"></div>
</div>
</div>
)}
</div>
</CardContent>
</Card>
</motion.div>
</div>
{/* Order Summary & Actions */}
<div className="space-y-6">
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 }}
>
<Card className="shadow-lg border-0 bg-gradient-to-br from-white to-gray-50">
<CardHeader>
<CardTitle className="text-xl">Order Summary</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-gray-600">Subtotal</span>
<span className="font-semibold">{order.total.toFixed(2)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600">Shipping</span>
<span className="text-green-600 font-semibold">Free</span>
</div>
<Separator />
<div className="flex justify-between items-center">
<span className="text-lg font-bold text-gray-900">Total</span>
<span className="text-xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
{order.total.toFixed(2)}
</span>
</div>
</div>
{order.razorpayOrderId && (
<div className="pt-4 border-t bg-gray-50 rounded-lg p-3">
<p className="text-sm text-gray-600 font-medium">Payment Details</p>
<p className="text-xs text-gray-500 mt-1">
ID: {order.razorpayPaymentId || 'Processing...'}
</p>
</div>
)}
{order.status === 'PENDING' && (
<Button
variant="destructive"
className="w-full bg-gradient-to-r from-red-500 to-pink-600 hover:from-red-600 hover:to-pink-700 shadow-lg"
onClick={handleCancelOrder}
disabled={cancelling}
>
{cancelling ? 'Cancelling...' : 'Cancel Order'}
</Button>
)}
</CardContent>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
>
<Card className="shadow-lg border-0 bg-gradient-to-br from-blue-50 to-indigo-50">
<CardHeader>
<CardTitle className="flex items-center text-xl">
<MapPin className="h-5 w-5 mr-3 text-blue-600" />
Customer Details
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center space-x-3 p-3 bg-white rounded-lg">
<Mail className="h-5 w-5 text-blue-500" />
<span className="text-sm font-medium">{order.user.email}</span>
</div>
<div className="flex items-center space-x-3 p-3 bg-white rounded-lg">
<Package className="h-5 w-5 text-green-500" />
<span className="text-sm font-medium">{order.user.name}</span>
</div>
</CardContent>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.6 }}
className="space-y-3"
>
<Button className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 shadow-lg" asChild>
<Link href="/orders">View All Orders</Link>
</Button>
<Button variant="outline" className="w-full border-2 hover:bg-gray-50" asChild>
<Link href="/products">Continue Shopping</Link>
</Button>
</motion.div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,246 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { Package, Truck, CheckCircle, XCircle, Clock, ArrowLeft, CreditCard } from 'lucide-react'
import { motion } from 'framer-motion'
import Link from 'next/link'
import Image from 'next/image'
import { toast } from 'sonner'
interface Order {
id: string
total: number
status: 'PENDING' | 'PAID' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED'
createdAt: string
orderItems: {
id: string
quantity: number
price: number
product: {
id: string
name: string
images: string[]
discount: number
}
}[]
}
const statusConfig = {
PENDING: { label: 'Pending', color: 'bg-gradient-to-r from-yellow-400 to-orange-500', icon: Clock },
PAID: { label: 'Paid', color: 'bg-gradient-to-r from-blue-500 to-indigo-600', icon: CreditCard },
SHIPPED: { label: 'Shipped', color: 'bg-gradient-to-r from-purple-500 to-pink-600', icon: Truck },
DELIVERED: { label: 'Delivered', color: 'bg-gradient-to-r from-green-500 to-emerald-600', icon: CheckCircle },
CANCELLED: { label: 'Cancelled', color: 'bg-gradient-to-r from-red-500 to-pink-600', icon: XCircle }
}
export default function OrdersPage() {
const router = useRouter()
const [orders, setOrders] = useState<Order[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const fetchOrders = useCallback(async () => {
try {
setLoading(true)
setError(null)
const response = await fetch('/api/orders')
if (!response.ok) {
if (response.status === 401) {
router.push('/auth/signin?callbackUrl=/orders')
return
}
throw new Error('Failed to fetch orders')
}
const data = await response.json()
setOrders(data)
} catch (error) {
console.error('Error fetching orders:', error)
setError('Failed to load orders. Please try again.')
} finally {
setLoading(false)
}
}, [router])
useEffect(() => {
fetchOrders()
}, [fetchOrders])
const handleCancelOrder = async (orderId: string) => {
try {
const response = await fetch(`/api/orders/${orderId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ status: 'CANCELLED' })
})
if (!response.ok) {
throw new Error('Failed to cancel order')
}
toast.success('Order cancelled successfully')
fetchOrders() // Refresh orders
} catch (error) {
console.error('Error cancelling order:', error)
toast.error('Failed to cancel order')
}
}
const handleReorder = (order: Order) => {
// Add all items from this order to cart
order.orderItems.forEach(item => {
// This would need the full product data to add to cart
console.log('Reordering:', item.product.name)
})
toast.success('Items added to cart!')
router.push('/checkout')
}
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
</div>
)
}
if (error) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
<Card className="max-w-md bg-white/80 backdrop-blur-sm shadow-xl border-0">
<CardContent className="text-center py-8">
<XCircle className="h-16 w-16 text-red-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">Error Loading Orders</h3>
<p className="text-gray-500 mb-4">{error}</p>
<Button onClick={fetchOrders} className="bg-gradient-to-r from-blue-600 to-purple-600">Try Again</Button>
</CardContent>
</Card>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<Button variant="ghost" asChild className="mb-4">
<Link href="/products">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Products
</Link>
</Button>
<h1 className="text-3xl font-bold bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent">My Orders</h1>
<p className="text-gray-600">Track and manage your orders</p>
</div>
{orders.length === 0 ? (
<Card className="bg-white/80 backdrop-blur-sm border-0 shadow-xl">
<CardContent className="text-center py-12">
<Package className="h-16 w-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No orders yet</h3>
<p className="text-gray-500 mb-6">Start shopping to see your orders here</p>
<Button asChild className="bg-gradient-to-r from-blue-600 to-purple-600">
<Link href="/products">Continue Shopping</Link>
</Button>
</CardContent>
</Card>
) : (
<div className="space-y-6">
{orders.map((order, index) => {
const StatusIcon = statusConfig[order.status].icon
return (
<motion.div
key={order.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card className="bg-white/90 backdrop-blur-sm border-0 shadow-lg hover:shadow-xl transition-shadow">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg">Order #{order.id.slice(-8)}</CardTitle>
<p className="text-sm text-gray-500">
{new Date(order.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
<Badge className={`${statusConfig[order.status].color} text-white shadow-lg`}>
<StatusIcon className="h-3 w-3 mr-1" />
{statusConfig[order.status].label}
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{order.orderItems.map((item) => (
<div key={item.id} className="flex items-center space-x-4 p-3 bg-gradient-to-r from-gray-50 to-white rounded-lg">
<Image
src={item.product.images[0] || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={item.product.name}
width={60}
height={60}
className="rounded-lg object-cover shadow-md"
/>
<div className="flex-1">
<h4 className="font-medium">{item.product.name}</h4>
<p className="text-sm text-gray-500">
Quantity: {item.quantity} × {item.price.toFixed(2)}
</p>
</div>
<div className="text-right">
<p className="font-bold text-lg bg-gradient-to-r from-green-600 to-emerald-600 bg-clip-text text-transparent">
{(item.price * item.quantity).toFixed(2)}
</p>
</div>
</div>
))}
<Separator />
<div className="flex justify-between items-center bg-gradient-to-r from-blue-50 to-purple-50 p-3 rounded-lg">
<span className="font-semibold">Total Amount</span>
<span className="font-bold text-xl bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">{order.total.toFixed(2)}</span>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" asChild>
<Link href={`/orders/${order.id}`}>View Details</Link>
</Button>
{order.status === 'DELIVERED' && (
<Button variant="outline" size="sm" onClick={() => handleReorder(order)} className="text-green-600">
Reorder
</Button>
)}
{order.status === 'PENDING' && (
<Button variant="outline" size="sm" onClick={() => handleCancelOrder(order.id)} className="text-red-600">
Cancel Order
</Button>
)}
</div>
</div>
</CardContent>
</Card>
</motion.div>
)
})}
</div>
)}
</div>
</div>
)
}

170
app/(public)/page.tsx Normal file
View File

@@ -0,0 +1,170 @@
import {
MobileOptimizedLazyHeroSection,
MobileOptimizedLazyOurValues,
MobileOptimizedLazyAboutSection,
MobileOptimizedLazyCertificationsSection,
MobileOptimizedLazyKashminaSection,
MobileOptimizedLazyManufacturingSection,
MobileOptimizedLazyStatsSection,
MobileOptimizedLazyNewsSection,
} from '@/components/MobileOptimizedLazyComponents'
import { Badge } from '@/components/ui/badge'
import { prisma } from '@/lib/prisma'
import ClientPageWrapper from '@/components/sections/ClientPageWrapper'
import ProductsSection from '@/components/sections/ProductsSection'
import { Metadata } from 'next'
import StructuredData, { MultipleStructuredData } from '@/components/StructuredData'
import { generateOrganizationJsonLd, generateProductListJsonLd } from '@/lib/structured-data'
export const metadata: Metadata = {
title: 'Padmaaja Rasooi - Premium Kashmina Rice & Quality Basmati | Home',
description: 'Experience the finest quality Kashmina Rice and premium basmati with Padmaaja Rasooi. Authentic Kashmina Steam and Sella rice varieties sourced from certified farms for exceptional taste, aroma and nutrition. Discover our sustainable farming practices.',
keywords: ['padmaaja rasooi', 'kashmina rice', 'kashmina basmati', 'kashmina steam rice', 'kashmina sella rice', 'premium rice', 'quality rice', 'organic rice', 'basmati rice', 'rice products', 'agriculture', 'quality grains', 'healthy food', 'sustainable farming', 'indian rice', 'authentic basmati'],
openGraph: {
title: 'Padmaaja Rasooi - Premium Rice Products & Quality Grains',
description: 'Experience the finest quality rice with Padmaaja Rasooi. Premium rice varieties sourced from the best farms for exceptional taste and nutrition.',
type: 'website',
images: ['/hero-bg.jpg'],
},
twitter: {
card: 'summary_large_image',
title: 'Padmaaja Rasooi - Premium Rice Products & Quality Grains',
description: 'Experience the finest quality rice with Padmaaja Rasooi. Premium rice varieties sourced from the best farms.',
images: ['/hero-bg.jpg'],
},
alternates: {
canonical: '/',
},
}
async function getProducts() {
try {
const products = await prisma.product.findMany({
where: {
isActive: true,
stock: {
gt: 0
}
},
include: {
category: true
},
orderBy: {
createdAt: 'desc'
},
take: 50 // Increased from 8 to show more products
})
return products
} catch (error) {
console.error('Failed to fetch products:', error)
return []
}
}
async function getCategories() {
try {
const categories = await prisma.category.findMany({
where: {
isActive: true,
products: {
some: {
isActive: true,
stock: {
gt: 0
}
}
}
},
orderBy: {
name: 'asc'
}
})
console.log('Fetched categories:', categories)
return categories
} catch (error) {
console.error('Failed to fetch categories:', error)
return []
}
}
export default async function Home() {
const products = await getProducts()
const categories = await getCategories()
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://padmaajarasooi.com'
// Generate JSON-LD structured data
const organizationJsonLd = generateOrganizationJsonLd(baseUrl)
const websiteJsonLd = {
'@context': 'https://schema.org',
'@type': 'WebSite',
'name': 'Padmaaja Rasooi',
'description': 'Premium Kashmina Rice and Quality Basmati Products',
'url': baseUrl,
'potentialAction': {
'@type': 'SearchAction',
'target': {
'@type': 'EntryPoint',
'urlTemplate': `${baseUrl}/products?search={search_term_string}`
},
'query-input': 'required name=search_term_string'
}
}
// Highlight Kashmina Rice products for SEO
const kashminaProducts = products.filter(p =>
p.name.toLowerCase().includes('kashmina') ||
p.brand?.toLowerCase().includes('kashmina')
).slice(0, 10)
const productListJsonLd = kashminaProducts.length > 0
? generateProductListJsonLd(kashminaProducts, baseUrl)
: null
return (
<ClientPageWrapper>
{/* SEO: Organization & Website Schema */}
<MultipleStructuredData
dataArray={[
organizationJsonLd,
websiteJsonLd,
...(productListJsonLd ? [productListJsonLd] : [])
]}
idPrefix="homepage"
/>
<main className="min-h-screen bg-white">
{/* Hero Section */}
<header>
<MobileOptimizedLazyHeroSection />
</header>
{/* Our Products Section */}
<ProductsSection products={products} categories={categories} />
{/* About Section */}
<MobileOptimizedLazyAboutSection />
{/* Kashmina Brand Section */}
<MobileOptimizedLazyKashminaSection />
{/* Manufacturing Excellence Section */}
<MobileOptimizedLazyManufacturingSection />
{/* Statistics & Achievements Section */}
{/* <MobileOptimizedLazyStatsSection /> */}
{/* Certifications Section */}
<MobileOptimizedLazyCertificationsSection />
{/* News & Updates Section */}
<MobileOptimizedLazyNewsSection />
{/* Our Values Section */}
<MobileOptimizedLazyOurValues />
</main>
</ClientPageWrapper>
)
}

View File

@@ -0,0 +1,383 @@
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Clock,
MapPin,
Users,
TrendingUp,
Shield,
Star,
CheckCircle,
Calendar,
DollarSign,
Award,
Target,
Zap,
Heart,
Briefcase,
Home,
Phone,
Mail,
FileText,
UserPlus
} from 'lucide-react'
import PageHero from '@/components/sections/PageHero'
import PartTimeRegistrationForm from '@/components/forms/PartTimeRegistrationForm'
export default function PartTimePage() {
const [showRegistrationModal, setShowRegistrationModal] = useState(false)
const jobBenefits = [
{
icon: DollarSign,
title: "Fixed Monthly Salary",
description: "Guaranteed ₹10,000 monthly salary regardless of sales"
},
{
icon: TrendingUp,
title: "1% Sales Incentive",
description: "Earn 1% commission on every successful sale you make"
},
{
icon: Clock,
title: "Flexible Hours",
description: "Work according to your available time schedule"
},
{
icon: Award,
title: "Sales Training",
description: "Free product knowledge and sales technique training"
}
]
const jobRole = {
id: 1,
icon: Target,
title: "Sales Executive",
description: "Sell premium Kashmiri rice products to customers",
requirements: [
"Good communication skills",
"Basic smartphone knowledge",
"3-4 hours daily availability",
"Willingness to learn"
],
salary: "₹10,000/month",
incentive: "1% commission on sales",
totalEarnings: "₹10,000 + Sales Commission",
color: "from-green-500 to-green-600"
}
const workProcess = [
{
step: 1,
icon: UserPlus,
title: "Apply Online",
description: "Fill out our simple registration form"
},
{
step: 2,
icon: Phone,
title: "Interview Call",
description: "Quick phone interview to assess your skills"
},
{
step: 3,
icon: Award,
title: "Training Provided",
description: "Free online training and skill development"
},
{
step: 4,
icon: Zap,
title: "Start Working",
description: "Begin your part-time job and earn money"
}
]
return (
<div className="min-h-screen bg-white">
<PageHero
title="Join Our Sales Team"
subtitle="Sales Executive Position"
description="₹10,000 guaranteed monthly salary + 1% commission. Flexible hours, comprehensive training, premium products."
backgroundGradient="from-green-600 to-emerald-700"
badge={{
text: "Now Hiring"
}}
actions={[
{
label: "Apply Now",
onClick: () => setShowRegistrationModal(true),
variant: "primary"
},
]}
/>
{/* Job Benefits */}
<section className="py-20 bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-4xl font-bold text-gray-900 mb-4"
>
Why Work With Us?
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-xl text-gray-600 max-w-3xl mx-auto"
>
Guaranteed salary + unlimited earning potential
</motion.p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{jobBenefits.map((benefit, index) => (
<motion.div
key={benefit.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="group"
>
<Card className="h-full hover:shadow-xl transition-all duration-300 border-0 shadow-lg group-hover:-translate-y-2">
<CardContent className="p-6 text-center">
<div className="w-16 h-16 bg-gradient-to-br from-purple-100 to-purple-200 rounded-2xl flex items-center justify-center mx-auto mb-4 group-hover:from-purple-500 group-hover:to-purple-600 transition-all duration-300">
<benefit.icon className="h-8 w-8 text-purple-600 group-hover:text-white transition-colors duration-300" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">
{benefit.title}
</h3>
<p className="text-gray-600">
{benefit.description}
</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* Available Job Roles */}
<section className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-4xl font-bold text-gray-900 mb-4"
>
Position Details
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-xl text-gray-600 max-w-3xl mx-auto"
>
Everything you need to know about the role
</motion.p>
</div>
<div className="flex justify-center">
<div className="max-w-md w-full">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="group relative"
>
<Card className="h-full hover:shadow-xl transition-all duration-300 border-0 shadow-lg group-hover:-translate-y-2">
<CardHeader className="pb-4">
<div className="flex items-center space-x-4">
<div className={`w-12 h-12 bg-gradient-to-br ${jobRole.color} rounded-xl flex items-center justify-center`}>
<jobRole.icon className="h-6 w-6 text-white" />
</div>
<div className="flex-1">
<CardTitle className="text-lg font-bold text-gray-900">
{jobRole.title}
</CardTitle>
<Badge className="bg-green-100 text-green-800 mt-1">
{jobRole.totalEarnings}
</Badge>
</div>
</div>
</CardHeader>
<CardContent className="pt-0">
<p className="text-gray-600 mb-4">
{jobRole.description}
</p>
<div className="mb-4">
<div className="flex items-center justify-between text-sm mb-2">
<span className="text-gray-600">Base Salary:</span>
<span className="font-semibold text-green-600">{jobRole.salary}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">Commission:</span>
<span className="font-semibold text-green-600">{jobRole.incentive}</span>
</div>
</div>
<h4 className="font-semibold text-gray-900 mb-2">Requirements:</h4>
<ul className="space-y-1">
{jobRole.requirements.map((req, reqIndex) => (
<li key={reqIndex} className="flex items-center text-sm text-gray-500">
<CheckCircle className="h-4 w-4 text-green-500 mr-2 flex-shrink-0" />
{req}
</li>
))}
</ul>
</CardContent>
</Card>
</motion.div>
</div>
</div>
</div>
</section>
{/* How It Works */}
<section className="py-20 bg-gradient-to-br from-purple-50 to-purple-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-4xl font-bold text-gray-900 mb-4"
>
How To Get Started
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-xl text-gray-600 max-w-3xl mx-auto"
>
Simple 4-step process to start earning
</motion.p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{workProcess.map((process, index) => (
<motion.div
key={process.step}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="text-center relative"
>
<div className="relative">
<div className="w-20 h-20 bg-white rounded-full shadow-lg flex items-center justify-center mx-auto mb-4">
<div className="w-16 h-16 bg-gradient-to-br from-purple-500 to-purple-600 rounded-full flex items-center justify-center">
<process.icon className="h-8 w-8 text-white" />
</div>
</div>
<div className="absolute -top-2 -right-2 w-8 h-8 bg-purple-600 text-white rounded-full flex items-center justify-center text-sm font-bold">
{process.step}
</div>
</div>
<h3 className="text-lg font-bold text-gray-900 mb-2">
{process.title}
</h3>
<p className="text-gray-600">
{process.description}
</p>
</motion.div>
))}
</div>
</div>
</section>
{/* Success Stories */}
<section className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-4xl font-bold text-gray-900 mb-4"
>
What Our Team Says
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-xl text-gray-600 max-w-3xl mx-auto"
>
Real experiences from our sales team
</motion.p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{[
{
name: "Priya Sharma",
role: "Sales Executive",
earnings: "₹13,500/month",
story: "The guaranteed salary of ₹10,000 plus commission gives me financial security. Great training and support!",
rating: 5
},
{
name: "Rahul Gupta",
role: "Sales Executive",
earnings: "₹15,200/month",
story: "I love the 1% commission structure. The more I sell, the more I earn. Management is very supportive.",
rating: 5
},
{
name: "Anjali Verma",
role: "Sales Executive",
earnings: "₹12,800/month",
story: "Perfect for someone who wants stable income with growth potential. Training helped me develop sales skills.",
rating: 5
}
].map((story, index) => (
<motion.div
key={story.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
>
<Card className="h-full hover:shadow-xl transition-all duration-300 border-0 shadow-lg">
<CardContent className="p-6">
<div className="flex items-center space-x-1 mb-4">
{[...Array(story.rating)].map((_, i) => (
<Star key={i} className="h-5 w-5 text-yellow-400 fill-current" />
))}
</div>
<p className="text-gray-600 mb-4 italic">"{story.story}"</p>
<div className="border-t pt-4">
<h4 className="font-bold text-gray-900">{story.name}</h4>
<p className="text-sm text-gray-500">{story.role}</p>
<Badge className="bg-green-100 text-green-800 mt-2">
Earning: {story.earnings}
</Badge>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* Part-Time Registration Modal */}
<PartTimeRegistrationForm
isOpen={showRegistrationModal}
onClose={() => setShowRegistrationModal(false)}
/>
</div>
)
}

View File

@@ -0,0 +1,377 @@
'use client'
import { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
import { Crown, Award, Star, Users, TrendingUp, ShoppingBag, ArrowRight, CheckCircle, AlertCircle } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import PageHero from '@/components/sections/PageHero'
import PartnershipApplicationForm from '@/components/forms/PartnershipApplicationForm'
interface TierAvailability {
name: string
limit: number
currentCount: number
remaining: number
available: boolean
percentageFull: number
}
export default function PartnerPage() {
const [isApplicationFormOpen, setIsApplicationFormOpen] = useState(false)
const [selectedTier, setSelectedTier] = useState('Silver')
const [tierAvailability, setTierAvailability] = useState<TierAvailability[]>([])
const [isLoading, setIsLoading] = useState(true)
// Fetch tier availability on component mount
useEffect(() => {
async function fetchTierAvailability() {
try {
const response = await fetch('/api/partnership/tiers')
const data = await response.json()
if (data.success) {
setTierAvailability(data.tiers)
}
} catch (error) {
console.error('Failed to fetch tier availability:', error)
} finally {
setIsLoading(false)
}
}
fetchTierAvailability()
}, [])
const partnershipTiers = [
{
name: 'Diamond',
icon: Crown,
maxUsers: 500,
color: '#3B82F6', // Blue for premium
bgColor: '#EFF6FF',
features: [
'Maximum 500 members',
'Highest commission rates',
'Premium marketing materials',
'Direct company support',
'Exclusive product launches',
'Priority customer service',
'Advanced analytics dashboard',
'Custom branding options'
],
benefits: 'First come, first served - No upgrades available'
},
{
name: 'Gold',
icon: Award,
maxUsers: 1500,
color: '#F59E0B', // Gold
bgColor: '#FFFBEB',
features: [
'Maximum 1,500 members',
'High commission rates',
'Standard marketing materials',
'Regular company support',
'Early product access',
'Priority support queue',
'Standard analytics',
'Co-branded materials'
],
benefits: 'Perfect for growing businesses'
},
{
name: 'Silver',
icon: Star,
maxUsers: 3000,
color: '#6B7280', // Silver gray
bgColor: '#F9FAFB',
features: [
'Maximum 3,000 members',
'Standard commission rates',
'Basic marketing materials',
'Email support',
'Regular product updates',
'Standard support',
'Basic analytics',
'Standard materials'
],
benefits: 'Great starting point for new partners'
}
]
const whyPartner = [
{
icon: TrendingUp,
title: 'Growing Market',
description: 'Be part of India&apos;s fastest-growing authentic food market with increasing demand for traditional products.'
},
{
icon: ShoppingBag,
title: 'Quality Products',
description: 'Market premium quality, FSSAI-certified traditional food products that customers trust and love.'
},
{
icon: Users,
title: 'Dedicated Support',
description: 'Get comprehensive support, training, and marketing materials to help you succeed.'
}
]
return (
<div className="min-h-screen">
{/* Hero Section */}
<PageHero
title="Marketing Partner"
subtitle="Be Our"
description="Join Padmaaja Rasooi's exclusive marketing partnership program and help us bring authentic Indian flavors to kitchens across the nation."
badge={{
text: "Partnership Opportunity"
}}
backgroundGradient="from-slate-900/90 via-emerald-900/80 to-slate-800/85"
titleGradient="from-orange-500 to-orange-400"
className="bg-gradient-to-r from-emerald-600/20 to-orange-500/20"
/>
{/* Partnership Tiers */}
<section className="py-20 bg-gradient-to-br from-slate-50 to-emerald-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
Choose Your Partnership Level
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto mb-8">
First come, first served basis - No upgrades available. Secure your preferred tier today!
</p>
<div className="inline-flex items-center px-4 py-2 bg-red-100 text-red-800 rounded-full text-sm font-medium">
<CheckCircle className="h-4 w-4 mr-2" />
Limited Spots Available - No Tier Upgrades
</div>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{partnershipTiers.map((tier, index) => {
const availability = tierAvailability.find(t => t.name === tier.name)
const isAvailable = availability?.available ?? true
const remaining = availability?.remaining ?? 0
return (
<motion.div
key={tier.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: index * 0.1 }}
className={index === 0 ? 'md:scale-105' : ''}
>
<Card className={`h-full border-2 shadow-xl hover:shadow-2xl transition-all duration-300 ${
index === 0 ? 'border-blue-300' : 'border-gray-200'
} ${!isAvailable ? 'opacity-75' : ''}`}>
<CardHeader className="text-center pb-4" style={{backgroundColor: tier.bgColor}}>
<div className="w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-4" style={{backgroundColor: `${tier.color}20`}}>
<tier.icon className="h-10 w-10" style={{color: tier.color}} />
</div>
<CardTitle className="text-2xl font-bold text-gray-900 mb-2">
{tier.name} Partner
</CardTitle>
<div className="text-3xl font-bold mb-2" style={{color: tier.color}}>
{tier.maxUsers.toLocaleString()} Users
</div>
{/* Availability Status */}
{!isLoading && availability && (
<div className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
isAvailable
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{isAvailable ? (
<>
<CheckCircle className="h-3 w-3 mr-1" />
{remaining} spots left
</>
) : (
<>
<AlertCircle className="h-3 w-3 mr-1" />
FULL
</>
)}
</div>
)}
<p className="text-sm text-gray-600 mt-2">
{tier.benefits}
</p>
</CardHeader>
<CardContent className="p-6">
<div className="space-y-3 mb-6">
{tier.features.map((feature, idx) => (
<div key={idx} className="flex items-start">
<CheckCircle className="h-5 w-5 text-emerald-500 mr-3 mt-0.5 flex-shrink-0" />
<span className="text-gray-600">{feature}</span>
</div>
))}
</div>
<Button
className="w-full text-white font-semibold"
style={{backgroundColor: isAvailable ? tier.color : '#9CA3AF'}}
disabled={!isAvailable}
onClick={() => {
if (isAvailable) {
setSelectedTier(tier.name)
setIsApplicationFormOpen(true)
}
}}
>
{isAvailable ? `Apply for ${tier.name}` : 'Tier Full'}
</Button>
</CardContent>
</Card>
</motion.div>
)
})}
</div>
</div>
</section>
{/* Why Partner With Us */}
<section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
Why Partner With Padmaaja Rasooi?
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Join a trusted brand that&apos;s revolutionizing the traditional food market in India.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{whyPartner.map((reason, index) => (
<motion.div
key={reason.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: index * 0.1 }}
>
<Card className="h-full border-0 shadow-lg hover:shadow-xl transition-all duration-300">
<CardContent className="p-8 text-center">
<div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-6" style={{backgroundColor: '#F5873B20'}}>
<reason.icon className="h-8 w-8" style={{color: '#F5873B'}} />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-4">
{reason.title}
</h3>
<p className="text-gray-600">
{reason.description}
</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* How It Works */}
<section className="py-20 bg-gradient-to-br from-slate-50 to-emerald-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
How Partnership Works
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Simple steps to become a Padmaaja Rasooi marketing partner.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{[
{ step: '1', title: 'Choose Tier', description: 'Select your preferred partnership tier based on customer capacity' },
{ step: '2', title: 'Apply', description: 'Submit your application with required documentation' },
{ step: '3', title: 'Get Approved', description: 'Our team reviews and approves qualified partners' },
{ step: '4', title: 'Start Marketing', description: 'Begin promoting our authentic food products to your network' }
].map((step, index) => (
<motion.div
key={step.step}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: index * 0.1 }}
className="text-center"
>
<div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{backgroundColor: '#F5873B', color: 'white'}}>
<span className="text-xl font-bold">{step.step}</span>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{step.title}
</h3>
<p className="text-gray-600">
{step.description}
</p>
</motion.div>
))}
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
Ready to Start Your Partnership Journey?
</h2>
<p className="text-xl text-gray-600 mb-8">
Don&apos;t miss this opportunity to be part of India&apos;s authentic food revolution.
Limited spots available on first-come, first-served basis.
</p>
<div className="space-y-4 sm:space-y-0 sm:space-x-4 sm:flex sm:justify-center">
<Button
size="lg"
className="w-full sm:w-auto text-lg px-8 py-4"
style={{backgroundColor: '#F5873B', color: 'white'}}
onClick={() => {
setSelectedTier('Silver')
setIsApplicationFormOpen(true)
}}
>
Apply Now <ArrowRight className="ml-2 h-5 w-5" />
</Button>
<Button
size="lg"
variant="outline"
className="w-full sm:w-auto text-lg px-8 py-4 border-gray-300"
>
Contact Us
</Button>
</div>
</motion.div>
</div>
</section>
{/* Partnership Application Form */}
<PartnershipApplicationForm
isOpen={isApplicationFormOpen}
onClose={() => setIsApplicationFormOpen(false)}
selectedTier={selectedTier}
/>
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,193 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Search, Loader2 } from 'lucide-react'
import ProductCard from '@/components/shop/ProductCard'
import StructuredData from '@/components/StructuredData'
import { generateProductListJsonLd, generateBreadcrumbJsonLd } from '@/lib/structured-data'
import { Product, Category } from '@/types'
import PageHero from '@/components/sections/PageHero'
export default function ProductsPage() {
const [products, setProducts] = useState<Product[]>([])
const [categories, setCategories] = useState<Category[]>([])
const [loading, setLoading] = useState(true)
const [loadingMore, setLoadingMore] = useState(false)
const [search, setSearch] = useState('')
const [selectedCategory, setSelectedCategory] = useState('all')
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [totalProducts, setTotalProducts] = useState(0)
const fetchProducts = useCallback(async (currentPage = 1, isLoadMore = false) => {
try {
if (!isLoadMore) {
setLoading(true)
} else {
setLoadingMore(true)
}
const params = new URLSearchParams({
page: currentPage.toString(),
limit: '12',
})
if (search) params.append('search', search)
if (selectedCategory && selectedCategory !== 'all') params.append('category', selectedCategory)
const response = await fetch(`/api/products?${params}`)
const data = await response.json()
if (isLoadMore) {
setProducts(prev => [...prev, ...data.products])
setHasMore(data.products.length === 12)
} else {
setProducts(data.products)
setHasMore(data.products.length === 12)
}
setTotalProducts(data.total || 0)
} catch (error) {
console.error('Error fetching products:', error)
} finally {
setLoading(false)
setLoadingMore(false)
}
}, [search, selectedCategory])
useEffect(() => {
fetchCategories()
fetchProducts(1, false)
}, [fetchProducts])
useEffect(() => {
setPage(1)
fetchProducts(1, false)
}, [search, selectedCategory, fetchProducts])
const fetchCategories = async () => {
try {
const response = await fetch('/api/categories')
const data = await response.json()
// Handle both old format (array) and new format (object with categories)
const categoriesArray = Array.isArray(data) ? data : data.categories || []
setCategories(categoriesArray)
} catch (error) {
console.error('Error fetching categories:', error)
}
}
const handleLoadMore = () => {
const nextPage = page + 1
setPage(nextPage)
fetchProducts(nextPage, true)
}
// Generate structured data
const baseUrl = process.env.NEXT_PUBLIC_URL || 'https://padmaajarasooi.com'
const productListData = generateProductListJsonLd(products, baseUrl)
const breadcrumbData = generateBreadcrumbJsonLd([
{ name: 'Home', url: '/' },
{ name: 'Products', url: '/products' }
], baseUrl)
return (
<div className="min-h-screen bg-gray-50">
{/* Structured Data */}
<StructuredData data={productListData} id="product-list-data" />
<StructuredData data={breadcrumbData} id="breadcrumb-data" />
{/* Hero Section */}
<PageHero
title="Products"
subtitle="Our"
description="Discover our premium collection of authentic food products, carefully crafted to bring traditional flavors to your kitchen."
badge={{
text: "Premium • Authentic • Quality"
}}
alignment="left"
/>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Filters */}
<div className="mb-8 flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search products..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10"
/>
</div>
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
<SelectTrigger className="w-full sm:w-48">
<SelectValue placeholder="All Categories" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
{categories.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Products Grid */}
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{[...Array(8)].map((_, i) => (
<div key={i} className="animate-pulse">
<div className="bg-gray-200 aspect-square rounded-lg mb-4"></div>
<div className="h-4 bg-gray-200 rounded mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-2/3"></div>
</div>
))}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{products.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
))}
</div>
)}
{products.length === 0 && !loading && (
<div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900 mb-2">No products found</h3>
<p className="text-gray-500">Try adjusting your search or filter criteria</p>
</div>
)}
{/* Load More Button */}
{!loading && hasMore && products.length > 0 && (
<div className="flex justify-center mt-12">
<Button
onClick={handleLoadMore}
disabled={loadingMore}
variant="outline"
size="lg"
className="px-8 py-3"
>
{loadingMore ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Loading...
</>
) : (
'Load More Products'
)}
</Button>
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,444 @@
'use client'
import { useState, useEffect } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { ArrowLeft, Plus, MapPin, Edit, Trash2, Star } from 'lucide-react'
import { motion } from 'framer-motion'
import { toast } from 'sonner'
import Link from 'next/link'
interface Address {
id: string
firstName: string
lastName: string
company?: string
address1: string
address2?: string
city: string
state: string
zipCode: string
country: string
phone?: string
isDefault: boolean
type: 'HOME' | 'WORK' | 'OTHER'
}
export default function AddressesPage() {
const [addresses, setAddresses] = useState<Address[]>([])
const [loading, setLoading] = useState(true)
const [dialogOpen, setDialogOpen] = useState(false)
const [editingAddress, setEditingAddress] = useState<Address | null>(null)
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
company: '',
address1: '',
address2: '',
city: '',
state: '',
zipCode: '',
country: 'India',
phone: '',
isDefault: false,
type: 'HOME' as 'HOME' | 'WORK' | 'OTHER'
})
useEffect(() => {
fetchAddresses()
}, [])
const fetchAddresses = async () => {
try {
const response = await fetch('/api/user/addresses')
if (response.ok) {
const data = await response.json()
setAddresses(data.addresses || [])
}
} catch (error) {
console.error('Error fetching addresses:', error)
toast.error('Failed to load addresses')
} finally {
setLoading(false)
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const requiredFields = ['firstName', 'lastName', 'address1', 'city', 'state', 'zipCode']
const missingFields = requiredFields.filter(field => !formData[field as keyof typeof formData])
if (missingFields.length > 0) {
toast.error('Please fill in all required fields')
return
}
try {
const url = editingAddress ? `/api/user/addresses/${editingAddress.id}` : '/api/user/addresses'
const method = editingAddress ? 'PUT' : 'POST'
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
if (!response.ok) throw new Error('Failed to save address')
toast.success(editingAddress ? 'Address updated successfully' : 'Address added successfully')
setDialogOpen(false)
resetForm()
fetchAddresses()
} catch (error) {
toast.error('Failed to save address')
}
}
const handleEdit = (address: Address) => {
setEditingAddress(address)
setFormData({
firstName: address.firstName,
lastName: address.lastName,
company: address.company || '',
address1: address.address1,
address2: address.address2 || '',
city: address.city,
state: address.state,
zipCode: address.zipCode,
country: address.country,
phone: address.phone || '',
isDefault: address.isDefault,
type: address.type
})
setDialogOpen(true)
}
const handleDelete = async (addressId: string) => {
if (!confirm('Are you sure you want to delete this address?')) return
try {
const response = await fetch(`/api/user/addresses/${addressId}`, { method: 'DELETE' })
if (!response.ok) throw new Error('Failed to delete address')
toast.success('Address deleted successfully')
fetchAddresses()
} catch (error) {
toast.error('Failed to delete address')
}
}
const handleSetDefault = async (addressId: string) => {
try {
const response = await fetch(`/api/user/addresses/${addressId}/default`, { method: 'PUT' })
if (!response.ok) throw new Error('Failed to set default address')
toast.success('Default address updated')
fetchAddresses()
} catch (error) {
toast.error('Failed to update default address')
}
}
const resetForm = () => {
setFormData({
firstName: '',
lastName: '',
company: '',
address1: '',
address2: '',
city: '',
state: '',
zipCode: '',
country: 'India',
phone: '',
isDefault: false,
type: 'HOME' as 'HOME' | 'WORK' | 'OTHER'
})
setEditingAddress(null)
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}))
}
if (loading) {
return (
<div className="min-h-screen bg-white flex items-center justify-center">
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
</div>
)
}
return (
<div className="min-h-screen bg-white">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="mb-8">
<Button variant="ghost" asChild className="mb-4">
<Link href="/dashboard/profile">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Profile
</Link>
</Button>
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">My Addresses</h1>
<p className="text-gray-600">Manage your shipping addresses</p>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button onClick={resetForm}>
<Plus className="h-4 w-4 mr-2" />
Add Address
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>
{editingAddress ? 'Edit Address' : 'Add New Address'}
</DialogTitle>
<DialogDescription>
{editingAddress ? 'Update your address details' : 'Add a new shipping address'}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="firstName">First Name *</Label>
<Input
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleInputChange}
required
/>
</div>
<div>
<Label htmlFor="lastName">Last Name *</Label>
<Input
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleInputChange}
required
/>
</div>
</div>
<div>
<Label htmlFor="company">Company (Optional)</Label>
<Input
id="company"
name="company"
value={formData.company}
onChange={handleInputChange}
/>
</div>
<div>
<Label htmlFor="address1">Address Line 1 *</Label>
<Input
id="address1"
name="address1"
value={formData.address1}
onChange={handleInputChange}
required
/>
</div>
<div>
<Label htmlFor="address2">Address Line 2 (Optional)</Label>
<Input
id="address2"
name="address2"
value={formData.address2}
onChange={handleInputChange}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="city">City *</Label>
<Input
id="city"
name="city"
value={formData.city}
onChange={handleInputChange}
required
/>
</div>
<div>
<Label htmlFor="state">State *</Label>
<Input
id="state"
name="state"
value={formData.state}
onChange={handleInputChange}
required
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="zipCode">ZIP Code *</Label>
<Input
id="zipCode"
name="zipCode"
value={formData.zipCode}
onChange={handleInputChange}
required
/>
</div>
<div>
<Label htmlFor="phone">Phone</Label>
<Input
id="phone"
name="phone"
value={formData.phone}
onChange={handleInputChange}
/>
</div>
</div>
<div>
<Label htmlFor="type">Address Type</Label>
<Select value={formData.type} onValueChange={(value) => setFormData(prev => ({ ...prev, type: value as 'HOME' | 'WORK' | 'OTHER' }))}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="HOME">Home</SelectItem>
<SelectItem value="WORK">Work</SelectItem>
<SelectItem value="OTHER">Other</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="isDefault"
name="isDefault"
checked={formData.isDefault}
onChange={handleInputChange}
className="rounded"
/>
<Label htmlFor="isDefault">Set as default address</Label>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>
Cancel
</Button>
<Button type="submit">
{editingAddress ? 'Update Address' : 'Add Address'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</div>
{/* Addresses List */}
{addresses.length === 0 ? (
<Card>
<CardContent className="text-center py-12">
<MapPin className="h-16 w-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No addresses found</h3>
<p className="text-gray-500 mb-6">Add your first address to get started</p>
<Button onClick={() => setDialogOpen(true)}>
<Plus className="h-4 w-4 mr-2" />
Add Address
</Button>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{addresses.map((address, index) => (
<motion.div
key={address.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
<Card className="relative">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<CardTitle className="text-lg">
{address.firstName} {address.lastName}
</CardTitle>
{address.isDefault && (
<Badge variant="default" className="bg-green-500">
<Star className="h-3 w-3 mr-1" />
Default
</Badge>
)}
</div>
<Badge variant="outline">{address.type}</Badge>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm text-gray-600">
{address.company && <p>{address.company}</p>}
<p>{address.address1}</p>
{address.address2 && <p>{address.address2}</p>}
<p>{address.city}, {address.state} {address.zipCode}</p>
<p>{address.country}</p>
{address.phone && <p>{address.phone}</p>}
</div>
<div className="flex items-center justify-between mt-4">
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handleEdit(address)}
>
<Edit className="h-3 w-3 mr-1" />
Edit
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDelete(address.id)}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="h-3 w-3 mr-1" />
Delete
</Button>
</div>
{!address.isDefault && (
<Button
variant="ghost"
size="sm"
onClick={() => handleSetDefault(address.id)}
className="text-blue-600"
>
Set as Default
</Button>
)}
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,6 @@
import { redirect } from 'next/navigation'
export default function ProfilePage() {
// Redirect to dashboard profile page
redirect('/dashboard/profile')
}

View File

@@ -0,0 +1,34 @@
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Rice Recipes | Padmaaja Rasooi - Authentic Indian Rice Dishes',
description: 'Discover delicious and authentic rice recipes from across India. From Vegetable Biryani to Rice Kheer, explore traditional dishes made with premium Basmati rice.',
keywords: 'rice recipes, basmati rice recipes, Indian rice dishes, vegetable biryani, rajma chawal, rice kheer, poha, khichdi, pulao, authentic Indian cooking',
openGraph: {
title: 'Rice Recipes | Padmaaja Rasooi',
description: 'Explore authentic Indian rice recipes made with premium Basmati rice. Step-by-step instructions for traditional dishes.',
type: 'website',
images: [
{
url: '/android-chrome-512x512.png',
width: 512,
height: 512,
alt: 'Padmaaja Rasooi Rice Recipes'
}
]
},
twitter: {
card: 'summary_large_image',
title: 'Authentic Rice Recipes | Padmaaja Rasooi',
description: 'Discover traditional Indian rice recipes with premium Basmati rice. Perfect for home cooking.',
images: ['/android-chrome-512x512.png']
}
}
export default function RecipesLayout({
children,
}: {
children: React.ReactNode
}) {
return children
}

View File

@@ -0,0 +1,205 @@
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
import Image from 'next/image'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Clock, Users, ChefHat, Eye } from 'lucide-react'
import { riceRecipes, Recipe } from '@/lib/recipe-data'
import RecipeDialog from '@/components/RecipeDialog'
import PageHero from '@/components/sections/PageHero'
import Link from 'next/link'
export default function RecipesPage() {
const [selectedRecipe, setSelectedRecipe] = useState<Recipe | null>(null)
const [isDialogOpen, setIsDialogOpen] = useState(false)
const handleViewRecipe = (recipe: Recipe) => {
setSelectedRecipe(recipe)
setIsDialogOpen(true)
}
const handleCloseDialog = () => {
setIsDialogOpen(false)
setSelectedRecipe(null)
}
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'Easy': return 'bg-green-100 text-green-800 border-green-200'
case 'Medium': return 'bg-yellow-100 text-yellow-800 border-yellow-200'
case 'Hard': return 'bg-red-100 text-red-800 border-red-200'
default: return 'bg-gray-100 text-gray-800 border-gray-200'
}
}
const getCategoryColor = (category: string) => {
switch (category) {
case 'Main Course': return 'bg-blue-100 text-blue-800 border-blue-200'
case 'Breakfast': return 'bg-orange-100 text-orange-800 border-orange-200'
case 'Dessert': return 'bg-pink-100 text-pink-800 border-pink-200'
default: return 'bg-gray-100 text-gray-800 border-gray-200'
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-amber-50/50 via-orange-50/30 to-yellow-50/20">
{/* Header Section */}
<PageHero
title="Rice Recipes"
description="Discover delicious and authentic rice recipes from across India. From comfort food to festive dishes, explore the versatility of premium rice with our curated collection."
titleGradient="secondary"
backgroundGradient="from-amber-50/50 via-orange-50/30 to-yellow-50/20"
icon={{
component: ChefHat,
bgColor: "bg-gradient-to-br from-amber-600 to-orange-600"
}}
features={[
{
icon: ChefHat,
label: "Premium Basmati Rice",
color: "orange"
},
{
icon: Users,
label: "Healthy & Nutritious",
color: "green"
},
{
icon: Eye,
label: "Authentic Recipes",
color: "orange"
}
]}
/>
{/* Recipes Grid */}
<section className="py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{riceRecipes.map((recipe, index) => (
<motion.div
key={recipe.id}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
>
<Card className="group h-full overflow-hidden bg-white/90 backdrop-blur-sm border-0 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div className="relative">
<div className="relative h-64 overflow-hidden">
<Image
src={recipe.image}
alt={recipe.name}
fill
className="object-cover group-hover:scale-110 transition-transform duration-700"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent"></div>
{/* Category Badge */}
<div className="absolute top-4 left-4">
<Badge className={`${getCategoryColor(recipe.category)} border`}>
{recipe.category}
</Badge>
</div>
{/* Difficulty Badge */}
<div className="absolute top-4 right-4">
<Badge className={`${getDifficultyColor(recipe.difficulty)} border`}>
{recipe.difficulty}
</Badge>
</div>
</div>
</div>
<CardContent className="p-6">
<div className="space-y-4">
<div>
<h3 className="text-xl font-bold text-gray-900 mb-2 group-hover:text-amber-700 transition-colors">
{recipe.name}
</h3>
<p className="text-gray-600 text-sm leading-relaxed line-clamp-2">
{recipe.description}
</p>
</div>
{/* Recipe Meta Info */}
<div className="flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
<Clock className="w-4 h-4 text-amber-600" />
<span className="font-medium">{recipe.cookTime}</span>
</div>
<div className="flex items-center gap-1">
<Users className="w-4 h-4 text-blue-600" />
<span className="font-medium">{recipe.servings}</span>
</div>
</div>
{recipe.nutritionInfo && (
<div className="text-xs bg-gray-100 px-2 py-1 rounded-full">
{recipe.nutritionInfo.calories} cal
</div>
)}
</div>
{/* View Recipe Button */}
<Button
onClick={() => handleViewRecipe(recipe)}
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-700 text-white shadow-lg hover:shadow-xl transition-all duration-300 group/btn"
>
<Eye className="w-4 h-4 mr-2 group-hover/btn:scale-110 transition-transform" />
View Recipe
</Button>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* Call to Action */}
<section className="py-16 bg-gradient-to-r from-amber-600 via-orange-600 to-red-600">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
className="space-y-6"
>
<h2 className="text-3xl md:text-4xl font-bold text-white">
Ready to Cook These Amazing Recipes?
</h2>
<p className="text-xl text-amber-100 leading-relaxed">
Get premium quality Basmati rice delivered fresh to your kitchen.
Perfect for all your cooking adventures!
</p>
<Link href="/products" className='mt-4 inline-block'>
<Button
size="lg"
variant="secondary"
className="bg-white text-amber-700 hover:bg-amber-50 font-semibold px-8 py-3 rounded-lg shadow-lg hover:shadow-xl transition-all duration-300"
>
<ChefHat className="w-5 h-5 mr-2" />
Shop Premium Rice
</Button>
</Link>
</motion.div>
</div>
</section>
{/* Recipe Dialog */}
<RecipeDialog
recipe={selectedRecipe}
isOpen={isDialogOpen}
onClose={handleCloseDialog}
/>
</div>
)
}

View File

@@ -0,0 +1,26 @@
import { LazySustainabilitySection } from '@/components/LazyComponents'
import PageHero from '@/components/sections/PageHero'
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Sustainability & CSR | Padmaaja Rasooi',
description: 'Our commitment to environmental stewardship and social responsibility. Discover our sustainability initiatives, CSR programs, and environmental certifications.',
keywords: 'sustainability, CSR, corporate social responsibility, environmental, organic farming, carbon neutral, zero waste',
}
export default function SustainabilityPage() {
return (
<div className="min-h-screen bg-white">
<PageHero
title="Sustainability & CSR"
subtitle="Building a Better Tomorrow"
description="Our commitment to environmental stewardship and social responsibility drives meaningful change in communities and ecosystems worldwide."
badge={{ text: "Our Impact" }}
backgroundGradient="from-green-600/10 to-emerald-600/10"
titleGradient="from-green-600 to-emerald-600"
/>
<LazySustainabilitySection />
</div>
)
}

View File

@@ -0,0 +1,379 @@
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import {
Users,
Package,
Send,
CheckCircle,
ShoppingCart,
Phone,
Handshake,
CreditCard,
Truck,
Gift,
TrendingUp,
Shield,
Store,
UserCheck,
FileText,
Star
} from 'lucide-react'
import PageHero from '@/components/sections/PageHero'
import WholesalerRegistrationForm from '@/components/forms/WholesalerRegistrationForm'
export default function WholesalerPage() {
const [showRegistrationModal, setShowRegistrationModal] = useState(false)
const wholesalerProcess = [
{
id: 1,
icon: UserCheck,
title: "Distributor Registration",
description: "Join our network as an authorized distributor",
details: ["Complete business verification", "Territory assignment", "Documentation setup", "Account activation"],
color: "from-blue-500 to-blue-600"
},
{
id: 2,
icon: Phone,
title: "Receive Initial Call",
description: "Our team contacts you for onboarding",
details: ["Partnership discussion", "Product catalog overview", "Pricing structure", "Support process"],
color: "from-green-500 to-green-600"
},
{
id: 3,
icon: Package,
title: "Sample Products Sent",
description: "Free quality samples delivered to your location",
details: ["Premium rice samples", "Quality testing", "Customer feedback", "Market evaluation"],
color: "from-purple-500 to-purple-600"
},
{
id: 4,
icon: CheckCircle,
title: "Quality Approval",
description: "Sample approval and order confirmation",
details: ["Quality validation", "Product selection", "Order specifications", "Quantity confirmation"],
color: "from-orange-500 to-orange-600"
},
{
id: 5,
icon: ShoppingCart,
title: "Place Bulk Orders",
description: "Order products directly from buyers",
details: ["Direct buyer orders", "Bulk quantities", "Competitive pricing", "Fast processing"],
color: "from-red-500 to-red-600"
},
{
id: 6,
icon: Phone,
title: "Call Buyers at Corporate Office",
description: "Connect buyers with our manufacturing unit",
details: ["Direct buyer contact", "Corporate office coordination", "Manufacturing visit", "Quality assurance"],
color: "from-indigo-500 to-indigo-600"
},
{
id: 7,
icon: Gift,
title: "Welcome Demo",
description: "Comprehensive product demonstration and agreement",
details: ["Product demonstration", "Partnership agreement", "Welcome package", "Support materials"],
color: "from-pink-500 to-pink-600"
},
{
id: 8,
icon: FileText,
title: "Formal Agreement",
description: "Sign official partnership agreement",
details: ["Contract terms", "Pricing agreement", "Territory rights", "Support commitments"],
color: "from-teal-500 to-teal-600"
},
{
id: 9,
icon: CreditCard,
title: "Payment Terms",
description: "100% advance payment with refundable deposit",
details: ["100% advance payment", "Refundable security", "Flexible terms", "Secure transactions"],
color: "from-cyan-500 to-cyan-600"
},
{
id: 10,
icon: Truck,
title: "Product Dispatch",
description: "100% quality products delivered",
details: ["Quality assurance", "Timely dispatch", "Secure packaging", "Tracking support"],
color: "from-emerald-500 to-emerald-600"
},
{
id: 11,
icon: TrendingUp,
title: "Low Cost Forever",
description: "Guaranteed lowest wholesale prices",
details: ["Competitive pricing", "Volume discounts", "Long-term benefits", "Price protection"],
color: "from-violet-500 to-violet-600"
},
{
id: 12,
icon: Shield,
title: "Guaranteed Products",
description: "100% quality assurance and satisfaction guarantee",
details: ["Quality guarantee", "Product authenticity", "Customer satisfaction", "Support assurance"],
color: "from-rose-500 to-rose-600"
}
]
const benefits = [
{
icon: TrendingUp,
title: "High Profit Margins",
description: "Earn substantial margins with our competitive wholesale pricing"
},
{
icon: Shield,
title: "Quality Guaranteed",
description: "100% quality assurance on all products with FSSAI certification"
},
{
icon: Users,
title: "Dedicated Support",
description: "Personal account manager for all your business needs"
},
{
icon: Truck,
title: "Reliable Supply",
description: "Consistent product availability and timely deliveries"
}
]
return (
<div className="min-h-screen bg-white">
<PageHero
title="Wholesaler Partnership Program"
description="Join India's leading Basmati rice manufacturer as an authorized distributor. Experience guaranteed profits with our comprehensive wholesaler program."
backgroundGradient="from-orange-600 to-orange-700"
badge={{
text: "Partnership Program",
variant: "outline"
}}
actions={[
{
label: "Register Now",
onClick: () => setShowRegistrationModal(true),
variant: "primary"
}
]}
/>
{/* Partnership Benefits */}
<section className="py-20 bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-4xl font-bold text-gray-900 mb-4"
>
Why Partner With Padmaaja Rasooi?
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-xl text-gray-600 max-w-3xl mx-auto"
>
Join our network of successful distributors and enjoy guaranteed profits with India's premium Basmati rice brand
</motion.p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{benefits.map((benefit, index) => (
<motion.div
key={benefit.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="group"
>
<Card className="h-full hover:shadow-xl transition-all duration-300 border-0 shadow-lg group-hover:-translate-y-2">
<CardContent className="p-6 text-center">
<div className="w-16 h-16 bg-gradient-to-br from-orange-100 to-orange-200 rounded-2xl flex items-center justify-center mx-auto mb-4 group-hover:from-orange-500 group-hover:to-orange-600 transition-all duration-300">
<benefit.icon className="h-8 w-8 text-orange-600 group-hover:text-white transition-colors duration-300" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">
{benefit.title}
</h3>
<p className="text-gray-600">
{benefit.description}
</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* Wholesaler Process Flow */}
<section className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-4xl font-bold text-gray-900 mb-4"
>
Wholesaler Partnership Process
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-xl text-gray-600 max-w-3xl mx-auto"
>
Our streamlined 12-step process ensures a smooth onboarding and profitable partnership
</motion.p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{wholesalerProcess.map((step, index) => (
<motion.div
key={step.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="group relative"
>
<Card className="h-full hover:shadow-xl transition-all duration-300 border-0 shadow-lg group-hover:-translate-y-2">
<CardHeader className="pb-4">
<div className="flex items-center space-x-4">
<div className={`w-12 h-12 bg-gradient-to-br ${step.color} rounded-xl flex items-center justify-center`}>
<step.icon className="h-6 w-6 text-white" />
</div>
<div className="flex-1">
<Badge variant="outline" className="text-xs font-semibold">
Step {step.id}
</Badge>
<CardTitle className="text-lg font-bold text-gray-900 mt-1">
{step.title}
</CardTitle>
</div>
</div>
</CardHeader>
<CardContent className="pt-0">
<p className="text-gray-600 mb-4">
{step.description}
</p>
<ul className="space-y-2">
{step.details.map((detail, detailIndex) => (
<li key={detailIndex} className="flex items-center text-sm text-gray-500">
<CheckCircle className="h-4 w-4 text-green-500 mr-2 flex-shrink-0" />
{detail}
</li>
))}
</ul>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* Investment & Returns */}
<section className="py-20 bg-gradient-to-br from-orange-50 to-orange-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-4xl font-bold text-gray-900 mb-4"
>
Investment & Returns
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-xl text-gray-600 max-w-3xl mx-auto"
>
Transparent pricing with guaranteed returns and low-cost forever commitment
</motion.p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<Card className="h-full bg-white shadow-xl border-0">
<CardContent className="p-8 text-center">
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl flex items-center justify-center mx-auto mb-6">
<CreditCard className="h-8 w-8 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">100% Advance Payment</h3>
<p className="text-gray-600 mb-4">
Secure your orders with full advance payment and enjoy priority processing
</p>
<Badge className="bg-blue-100 text-blue-800">Refundable Deposit</Badge>
</CardContent>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
<Card className="h-full bg-white shadow-xl border-0">
<CardContent className="p-8 text-center">
<div className="w-16 h-16 bg-gradient-to-br from-green-500 to-green-600 rounded-2xl flex items-center justify-center mx-auto mb-6">
<TrendingUp className="h-8 w-8 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">Low Cost Forever</h3>
<p className="text-gray-600 mb-4">
Guaranteed lowest wholesale prices with no hidden charges or price increases
</p>
<Badge className="bg-green-100 text-green-800">Price Protection</Badge>
</CardContent>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
>
<Card className="h-full bg-white shadow-xl border-0">
<CardContent className="p-8 text-center">
<div className="w-16 h-16 bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl flex items-center justify-center mx-auto mb-6">
<Shield className="h-8 w-8 text-white" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">100% Guaranteed</h3>
<p className="text-gray-600 mb-4">
Complete quality assurance and satisfaction guarantee on all products
</p>
<Badge className="bg-purple-100 text-purple-800">Quality Assured</Badge>
</CardContent>
</Card>
</motion.div>
</div>
</div>
</section>
{/* Wholesaler Registration Modal */}
<WholesalerRegistrationForm
isOpen={showRegistrationModal}
onClose={() => setShowRegistrationModal(false)}
/>
</div>
)
}

View File

@@ -0,0 +1,301 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Search, Plus, Edit, Trash2, MoreHorizontal, Download } from 'lucide-react'
import { toast } from 'sonner'
import Link from 'next/link'
import Image from 'next/image'
import { CategoryFormDialog } from '@/components/admin/CategoryFormDialog'
interface Category {
id: string
name: string
description: string
image: string | null
isActive: boolean
createdAt: string
_count: {
products: number
}
}
export default function AdminCategoriesPage() {
const [categories, setCategories] = useState<Category[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState('')
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
const [bulkActionLoading, setBulkActionLoading] = useState(false)
const fetchCategories = useCallback(async () => {
try {
setLoading(true)
const params = new URLSearchParams()
if (search) params.append('search', search)
const response = await fetch(`/api/admin/categories?${params}`)
const data = await response.json()
setCategories(data || [])
} catch (error) {
console.error('Error fetching categories:', error)
toast.error('Failed to load categories')
} finally {
setLoading(false)
}
}, [search])
useEffect(() => {
fetchCategories()
}, [fetchCategories])
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedCategories(categories.map(c => c.id))
} else {
setSelectedCategories([])
}
}
const handleSelectCategory = (categoryId: string, checked: boolean) => {
if (checked) {
setSelectedCategories(prev => [...prev, categoryId])
} else {
setSelectedCategories(prev => prev.filter(id => id !== categoryId))
}
}
const handleBulkAction = async (action: 'activate' | 'deactivate' | 'delete') => {
if (selectedCategories.length === 0) {
toast.error('Please select at least one category')
return
}
setBulkActionLoading(true)
try {
const response = await fetch('/api/admin/categories/bulk', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
categoryIds: selectedCategories,
action
})
})
if (!response.ok) throw new Error('Bulk action failed')
toast.success(`Successfully ${action}d ${selectedCategories.length} categories`)
setSelectedCategories([])
fetchCategories()
} catch (error) {
toast.error(`Failed to ${action} categories`)
} finally {
setBulkActionLoading(false)
}
}
const handleDeleteCategory = async (categoryId: string) => {
if (!confirm('Are you sure you want to delete this category?')) return
try {
const response = await fetch(`/api/admin/categories/${categoryId}`, {
method: 'DELETE'
})
if (!response.ok) throw new Error('Delete failed')
toast.success('Category deleted successfully')
fetchCategories()
} catch (error) {
toast.error('Failed to delete category')
}
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Categories</h1>
<p className="text-gray-600">Manage your product categories</p>
</div>
<CategoryFormDialog onSuccess={fetchCategories} />
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>All Categories</CardTitle>
<div className="flex items-center space-x-2">
{selectedCategories.length > 0 && (
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-500">
{selectedCategories.length} selected
</span>
<Button
variant="outline"
size="sm"
onClick={() => handleBulkAction('activate')}
disabled={bulkActionLoading}
>
Activate
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleBulkAction('deactivate')}
disabled={bulkActionLoading}
>
Deactivate
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleBulkAction('delete')}
disabled={bulkActionLoading}
className="text-red-600"
>
Delete
</Button>
</div>
)}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search categories..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 w-64"
/>
</div>
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
Export
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="text-center py-8">Loading...</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={selectedCategories.length === categories.length && categories.length > 0}
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>Category</TableHead>
<TableHead>Description</TableHead>
<TableHead>Products</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{categories.map((category) => (
<TableRow key={category.id}>
<TableCell>
<Checkbox
checked={selectedCategories.includes(category.id)}
onCheckedChange={(checked) => handleSelectCategory(category.id, !!checked)}
/>
</TableCell>
<TableCell>
<div className="flex items-center space-x-3">
{category.image ? (
<Image
src={category.image}
alt={category.name}
width={40}
height={40}
className="rounded-lg object-cover"
/>
) : (
<div className="w-10 h-10 bg-gray-200 rounded-lg flex items-center justify-center">
<span className="text-gray-400 text-xs">No Image</span>
</div>
)}
<div>
<p className="font-medium">{category.name}</p>
</div>
</div>
</TableCell>
<TableCell>
<p className="text-sm text-gray-600 max-w-xs truncate">
{category.description || 'No description'}
</p>
</TableCell>
<TableCell>
<Badge variant="secondary">
{category._count.products} products
</Badge>
</TableCell>
<TableCell>
<Badge variant={category.isActive ? 'default' : 'secondary'}>
{category.isActive ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell>
{new Date(category.createdAt).toLocaleDateString()}
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<CategoryFormDialog
category={category}
mode="edit"
onSuccess={fetchCategories}
trigger={
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<Edit className="h-4 w-4 mr-2" />
Edit
</DropdownMenuItem>
}
/>
<DropdownMenuItem
onClick={() => handleDeleteCategory(category.id)}
className="text-red-600"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,525 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
DollarSign,
TrendingUp,
Users,
Settings,
Search,
Filter,
CheckCircle,
Clock,
XCircle,
Edit,
Plus
} from 'lucide-react'
import { motion } from 'framer-motion'
import { toast } from 'sonner'
interface Commission {
id: string
amount: number
level: number
type: 'REFERRAL' | 'LEVEL' | 'BONUS'
status: 'PENDING' | 'APPROVED' | 'PAID' | 'CANCELLED'
createdAt: string
user: {
id: string
name: string
email: string
}
fromUser: {
id: string
name: string
email: string
}
order?: {
id: string
total: number
}
}
interface CommissionStats {
totalCommissions: number
pendingAmount: number
approvedAmount: number
paidAmount: number
thisMonthCommissions: number
averageCommission: number
}
interface CommissionSetting {
id: string
level: number
percentage: number
isActive: boolean
}
const statusConfig = {
PENDING: { label: 'Pending', color: 'bg-yellow-100 text-yellow-800', icon: Clock },
APPROVED: { label: 'Approved', color: 'bg-blue-100 text-blue-800', icon: CheckCircle },
PAID: { label: 'Paid', color: 'bg-green-100 text-green-800', icon: CheckCircle },
CANCELLED: { label: 'Cancelled', color: 'bg-red-100 text-red-800', icon: XCircle }
}
export default function AdminCommissionsPage() {
const [commissions, setCommissions] = useState<Commission[]>([])
const [stats, setStats] = useState<CommissionStats | null>(null)
const [settings, setSettings] = useState<CommissionSetting[]>([])
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState({
status: 'all',
type: 'all',
level: 'all',
search: ''
})
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false)
const [editingSetting, setEditingSetting] = useState<CommissionSetting | null>(null)
const fetchData = useCallback(async () => {
try {
setLoading(true)
const [commissionsRes, statsRes, settingsRes] = await Promise.all([
fetch('/api/admin/commissions'),
fetch('/api/admin/commissions/stats'),
fetch('/api/admin/commissions/settings')
])
// Check if responses are ok and contain valid JSON
const commissionsData = commissionsRes.ok ? await commissionsRes.json() : { commissions: [] }
const statsData = statsRes.ok ? await statsRes.json() : {
totalCommissions: 0,
pendingAmount: 0,
approvedAmount: 0,
paidAmount: 0,
thisMonthCommissions: 0,
averageCommission: 0
}
const settingsData = settingsRes.ok ? await settingsRes.json() : { settings: [] }
setCommissions(commissionsData.commissions || [])
setStats(statsData)
setSettings(settingsData.settings || [])
} catch (error) {
console.error('Error fetching commission data:', error)
toast.error('Failed to load commission data')
// Set fallback data
setCommissions([])
setStats({
totalCommissions: 0,
pendingAmount: 0,
approvedAmount: 0,
paidAmount: 0,
thisMonthCommissions: 0,
averageCommission: 0
})
setSettings([])
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchData()
}, [fetchData])
const handleStatusUpdate = async (commissionId: string, newStatus: string) => {
try {
const response = await fetch(`/api/admin/commissions/${commissionId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus })
})
if (!response.ok) throw new Error('Failed to update commission')
toast.success('Commission status updated successfully')
fetchData()
} catch (error) {
console.error('Error updating commission:', error)
toast.error('Failed to update commission status')
}
}
const handleSettingUpdate = async (setting: CommissionSetting) => {
try {
const response = await fetch(`/api/admin/commissions/settings/${setting.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
level: setting.level,
percentage: setting.percentage,
isActive: setting.isActive
})
})
if (!response.ok) throw new Error('Failed to update setting')
toast.success('Commission setting updated successfully')
fetchData()
setEditingSetting(null)
setSettingsDialogOpen(false)
} catch (error) {
console.error('Error updating setting:', error)
toast.error('Failed to update commission setting')
}
}
const filteredCommissions = commissions.filter(commission => {
const matchesStatus = filter.status === 'all' || commission.status === filter.status
const matchesType = filter.type === 'all' || commission.type === filter.type
const matchesLevel = filter.level === 'all' || commission.level.toString() === filter.level
const matchesSearch = !filter.search ||
commission.user.name.toLowerCase().includes(filter.search.toLowerCase()) ||
commission.user.email.toLowerCase().includes(filter.search.toLowerCase()) ||
commission.fromUser.name.toLowerCase().includes(filter.search.toLowerCase())
return matchesStatus && matchesType && matchesLevel && matchesSearch
})
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100 p-6">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6"
>
<div>
<h1 className="text-4xl font-bold bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent">
Commission Management
</h1>
<p className="text-gray-600 mt-2">Monitor and manage all commission transactions</p>
</div>
<Dialog open={settingsDialogOpen} onOpenChange={setSettingsDialogOpen}>
<DialogTrigger asChild>
<Button className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 shadow-lg">
<Settings className="h-4 w-4 mr-2" />
Commission Settings
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Commission Settings</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{settings.map((setting) => (
<div key={setting.id} className="flex items-center justify-between p-4 border rounded-lg">
<div>
<p className="font-medium">Level {setting.level}</p>
<p className="text-sm text-gray-500">{setting.percentage}% commission</p>
</div>
<div className="flex items-center space-x-2">
<Badge variant={setting.isActive ? 'default' : 'secondary'}>
{setting.isActive ? 'Active' : 'Inactive'}
</Badge>
<Button
variant="outline"
size="sm"
onClick={() => setEditingSetting(setting)}
>
<Edit className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</DialogContent>
</Dialog>
</motion.div>
{/* Stats Cards */}
{stats && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"
>
<Card className="bg-gradient-to-r from-blue-500 to-blue-600 text-white border-0 shadow-lg">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-sm">Total Commissions</p>
<p className="text-2xl font-bold">{stats.totalCommissions}</p>
</div>
<Users className="h-8 w-8 text-blue-200" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-yellow-500 to-yellow-600 text-white border-0 shadow-lg">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-yellow-100 text-sm">Pending Amount</p>
<p className="text-2xl font-bold">{stats.pendingAmount.toFixed(0)}</p>
</div>
<Clock className="h-8 w-8 text-yellow-200" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-green-500 to-green-600 text-white border-0 shadow-lg">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-green-100 text-sm">Paid Amount</p>
<p className="text-2xl font-bold">{stats.paidAmount.toFixed(0)}</p>
</div>
<CheckCircle className="h-8 w-8 text-green-200" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-purple-500 to-purple-600 text-white border-0 shadow-lg">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-purple-100 text-sm">Avg Commission</p>
<p className="text-2xl font-bold">{stats.averageCommission.toFixed(0)}</p>
</div>
<TrendingUp className="h-8 w-8 text-purple-200" />
</div>
</CardContent>
</Card>
</motion.div>
)}
{/* Main Commission Table */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<Card className="bg-white/90 backdrop-blur-sm border-0 shadow-xl">
<CardHeader>
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
<CardTitle className="text-2xl font-bold">All Commissions</CardTitle>
<div className="flex flex-wrap items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search users..."
value={filter.search}
onChange={(e) => setFilter(prev => ({ ...prev, search: e.target.value }))}
className="pl-10 w-64 bg-white shadow-sm"
/>
</div>
<Select value={filter.status} onValueChange={(value) => setFilter(prev => ({ ...prev, status: value }))}>
<SelectTrigger className="w-40 bg-white shadow-sm">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="PENDING">Pending</SelectItem>
<SelectItem value="APPROVED">Approved</SelectItem>
<SelectItem value="PAID">Paid</SelectItem>
<SelectItem value="CANCELLED">Cancelled</SelectItem>
</SelectContent>
</Select>
<Select value={filter.type} onValueChange={(value) => setFilter(prev => ({ ...prev, type: value }))}>
<SelectTrigger className="w-40 bg-white shadow-sm">
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="REFERRAL">Referral</SelectItem>
<SelectItem value="LEVEL">Level</SelectItem>
<SelectItem value="BONUS">Bonus</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
<div className="overflow-hidden rounded-lg border border-gray-200">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="font-semibold">Recipient</TableHead>
<TableHead className="font-semibold">From User</TableHead>
<TableHead className="font-semibold">Amount</TableHead>
<TableHead className="font-semibold">Level</TableHead>
<TableHead className="font-semibold">Type</TableHead>
<TableHead className="font-semibold">Status</TableHead>
<TableHead className="font-semibold">Date</TableHead>
<TableHead className="font-semibold">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredCommissions.map((commission, index) => (
<motion.tr
key={commission.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className="hover:bg-gray-50 transition-colors"
>
<TableCell>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-r from-blue-500 to-purple-600 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-medium">
{commission.user.name?.charAt(0) || 'U'}
</span>
</div>
<div>
<p className="font-medium text-gray-900">{commission.user.name}</p>
<p className="text-sm text-gray-500">{commission.user.email}</p>
</div>
</div>
</TableCell>
<TableCell>
<div>
<p className="font-medium text-gray-900">{commission.fromUser.name}</p>
<p className="text-sm text-gray-500">{commission.fromUser.email}</p>
</div>
</TableCell>
<TableCell>
<span className="font-bold text-lg bg-gradient-to-r from-green-600 to-emerald-600 bg-clip-text text-transparent">
{commission.amount.toFixed(2)}
</span>
</TableCell>
<TableCell>
<Badge variant="outline">Level {commission.level}</Badge>
</TableCell>
<TableCell>
<Badge variant="secondary">{commission.type}</Badge>
</TableCell>
<TableCell>
<Badge className={statusConfig[commission.status].color}>
{statusConfig[commission.status].label}
</Badge>
</TableCell>
<TableCell>
<p className="text-sm">
{new Date(commission.createdAt).toLocaleDateString()}
</p>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
{commission.status === 'PENDING' && (
<>
<Button
size="sm"
variant="outline"
onClick={() => handleStatusUpdate(commission.id, 'APPROVED')}
className="text-blue-600 hover:bg-blue-50"
>
Approve
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleStatusUpdate(commission.id, 'CANCELLED')}
className="text-red-600 hover:bg-red-50"
>
Reject
</Button>
</>
)}
{commission.status === 'APPROVED' && (
<Button
size="sm"
variant="outline"
onClick={() => handleStatusUpdate(commission.id, 'PAID')}
className="text-green-600 hover:bg-green-50"
>
Mark Paid
</Button>
)}
</div>
</TableCell>
</motion.tr>
))}
</TableBody>
</Table>
</div>
{filteredCommissions.length === 0 && (
<div className="text-center py-12">
<DollarSign className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No commissions found</h3>
<p className="text-gray-500">Try adjusting your search or filter criteria</p>
</div>
)}
</CardContent>
</Card>
</motion.div>
{/* Edit Setting Dialog */}
{editingSetting && (
<Dialog open={!!editingSetting} onOpenChange={() => setEditingSetting(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Commission Setting</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Level</Label>
<Input
type="number"
value={editingSetting.level}
onChange={(e) => setEditingSetting(prev => prev ? { ...prev, level: parseInt(e.target.value) } : null)}
/>
</div>
<div>
<Label>Percentage (%)</Label>
<Input
type="number"
step="0.1"
value={editingSetting.percentage}
onChange={(e) => setEditingSetting(prev => prev ? { ...prev, percentage: parseFloat(e.target.value) } : null)}
/>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="isActive"
checked={editingSetting.isActive}
onChange={(e) => setEditingSetting(prev => prev ? { ...prev, isActive: e.target.checked } : null)}
/>
<Label htmlFor="isActive">Active</Label>
</div>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={() => setEditingSetting(null)}>
Cancel
</Button>
<Button onClick={() => handleSettingUpdate(editingSetting)}>
Save Changes
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)}
</div>
</div>
)
}

646
app/admin/forms/page.tsx Normal file
View File

@@ -0,0 +1,646 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Input } from '@/components/ui/input'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Mail,
Search,
Filter,
Eye,
Calendar,
User,
Building,
Phone,
MessageSquare,
ExternalLink
} from 'lucide-react'
import { toast } from 'sonner'
interface FormResponse {
id: string
formId: string
data: any // Changed from specific interface to any to handle different form types
metadata?: {
ipAddress?: string
userAgent?: string
referrer?: string
timestamp?: string
}
status: string
createdAt: string
updatedAt: string
}
interface Pagination {
page: number
limit: number
total: number
pages: number
}
export default function AdminFormsPage() {
const [forms, setForms] = useState<FormResponse[]>([])
const [loading, setLoading] = useState(true)
const [selectedForm, setSelectedForm] = useState<FormResponse | null>(null)
const [showDetails, setShowDetails] = useState(false)
const [pagination, setPagination] = useState<Pagination>({
page: 1,
limit: 10,
total: 0,
pages: 0
})
// Filters
const [statusFilter, setStatusFilter] = useState('all')
const [formTypeFilter, setFormTypeFilter] = useState('all')
const [searchQuery, setSearchQuery] = useState('')
const fetchForms = useCallback(async () => {
try {
setLoading(true)
const params = new URLSearchParams({
page: pagination.page.toString(),
limit: pagination.limit.toString(),
})
if (statusFilter !== 'all') {
params.append('status', statusFilter)
}
if (formTypeFilter !== 'all') {
params.append('formId', formTypeFilter)
}
const response = await fetch(`/api/forms?${params}`)
const result = await response.json()
if (result.success) {
setForms(result.data)
setPagination(result.pagination)
} else {
toast.error('Failed to fetch form responses')
}
} catch (error) {
console.error('Error fetching forms:', error)
toast.error('Failed to fetch form responses')
} finally {
setLoading(false)
}
}, [pagination.page, pagination.limit, statusFilter, formTypeFilter])
useEffect(() => {
fetchForms()
}, [fetchForms])
const updateFormStatus = async (formId: string, newStatus: string) => {
try {
const response = await fetch(`/api/forms/${formId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: newStatus })
})
if (response.ok) {
toast.success('Status updated successfully')
fetchForms()
if (selectedForm && selectedForm.id === formId) {
setSelectedForm({ ...selectedForm, status: newStatus })
}
} else {
toast.error('Failed to update status')
}
} catch (error) {
console.error('Error updating status:', error)
toast.error('Failed to update status')
}
}
const getStatusBadge = (status: string) => {
const statusColors = {
new: 'bg-blue-100 text-blue-800',
in_progress: 'bg-yellow-100 text-yellow-800',
resolved: 'bg-green-100 text-green-800',
closed: 'bg-gray-100 text-gray-800'
}
return (
<Badge className={statusColors[status as keyof typeof statusColors] || 'bg-gray-100 text-gray-800'}>
{status.replace('_', ' ').toUpperCase()}
</Badge>
)
}
const getFormTypeBadge = (formId: string) => {
const typeColors = {
contact: 'bg-purple-100 text-purple-800',
partnership: 'bg-indigo-100 text-indigo-800',
bulk_inquiry: 'bg-orange-100 text-orange-800',
support: 'bg-teal-100 text-teal-800'
}
return (
<Badge variant="outline" className={typeColors[formId as keyof typeof typeColors]}>
{formId.replace('_', ' ').toUpperCase()}
</Badge>
)
}
const filteredForms = forms.filter(form => {
if (searchQuery) {
const query = searchQuery.toLowerCase()
const data = form.data
// Handle different form types
if (form.formId === 'partnership_application') {
return (
data.firstName?.toLowerCase().includes(query) ||
data.lastName?.toLowerCase().includes(query) ||
data.email?.toLowerCase().includes(query) ||
data.businessName?.toLowerCase().includes(query) ||
data.partnershipTier?.toLowerCase().includes(query) ||
data.motivation?.toLowerCase().includes(query)
)
} else {
// Contact forms and other types
return (
data.name?.toLowerCase().includes(query) ||
data.email?.toLowerCase().includes(query) ||
data.message?.toLowerCase().includes(query) ||
data.subject?.toLowerCase().includes(query)
)
}
}
return true
})
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Form Responses</h1>
<p className="text-muted-foreground">
Manage and respond to customer inquiries and form submissions
</p>
</div>
</div>
{/* Filters */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Filter className="h-5 w-5" />
Filters
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by name, email, or message..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-64"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="new">New</SelectItem>
<SelectItem value="in_progress">In Progress</SelectItem>
<SelectItem value="resolved">Resolved</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
</SelectContent>
</Select>
<Select value={formTypeFilter} onValueChange={setFormTypeFilter}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Form Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="contact">Contact</SelectItem>
<SelectItem value="partnership">Partnership</SelectItem>
<SelectItem value="bulk_inquiry">Bulk Inquiry</SelectItem>
<SelectItem value="support">Support</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Form Responses Table */}
<Card>
<CardHeader>
<CardTitle>
Form Responses ({pagination.total})
</CardTitle>
<CardDescription>
Click on any row to view detailed information and respond to inquiries
</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
) : (
<div className="space-y-4">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Type</TableHead>
<TableHead>Status</TableHead>
<TableHead>Subject</TableHead>
<TableHead>Date</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredForms.map((form) => {
// Handle different form types for display
const displayName = form.formId === 'partnership_application'
? `${form.data.firstName || ''} ${form.data.lastName || ''}`.trim()
: form.data.name || 'Unknown'
const displayEmail = form.data.email || 'No email'
const displaySubject = form.formId === 'partnership_application'
? `${form.data.partnershipTier || 'Unknown'} Partnership Application`
: form.data.subject || form.data.inquiryType || 'No subject'
return (
<TableRow key={form.id} className="cursor-pointer hover:bg-muted/50">
<TableCell className="font-medium">{displayName}</TableCell>
<TableCell>{displayEmail}</TableCell>
<TableCell>{getFormTypeBadge(form.formId)}</TableCell>
<TableCell>{getStatusBadge(form.status)}</TableCell>
<TableCell className="max-w-xs truncate">
{displaySubject}
</TableCell>
<TableCell>
{new Date(form.createdAt).toLocaleDateString()}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedForm(form)
setShowDetails(true)
}}
>
<Eye className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
{/* Pagination */}
{pagination.pages > 1 && (
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Showing {((pagination.page - 1) * pagination.limit) + 1} to{' '}
{Math.min(pagination.page * pagination.limit, pagination.total)} of{' '}
{pagination.total} entries
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setPagination(p => ({ ...p, page: p.page - 1 }))}
disabled={pagination.page === 1}
>
Previous
</Button>
<span className="text-sm">
Page {pagination.page} of {pagination.pages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setPagination(p => ({ ...p, page: p.page + 1 }))}
disabled={pagination.page === pagination.pages}
>
Next
</Button>
</div>
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* Form Details Dialog */}
<Dialog open={showDetails} onOpenChange={setShowDetails}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Mail className="h-5 w-5" />
Form Response Details
</DialogTitle>
<DialogDescription>
View and manage this form submission
</DialogDescription>
</DialogHeader>
{selectedForm && (
<div className="space-y-6">
{/* Header Info */}
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
{getFormTypeBadge(selectedForm.formId)}
{getStatusBadge(selectedForm.status)}
</div>
<p className="text-sm text-muted-foreground">
Submitted on {new Date(selectedForm.createdAt).toLocaleDateString()} at{' '}
{new Date(selectedForm.createdAt).toLocaleTimeString()}
</p>
</div>
<Select
value={selectedForm.status}
onValueChange={(newStatus) => updateFormStatus(selectedForm.id, newStatus)}
>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="new">New</SelectItem>
<SelectItem value="in_progress">In Progress</SelectItem>
<SelectItem value="resolved">Resolved</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
</SelectContent>
</Select>
</div>
{/* Contact Information */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Contact Information</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Name:</span>
<span>
{selectedForm.formId === 'partnership_application'
? `${selectedForm.data.firstName || ''} ${selectedForm.data.lastName || ''}`.trim()
: selectedForm.data.name || 'Unknown'
}
</span>
</div>
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Email:</span>
<a
href={`mailto:${selectedForm.data.email}`}
className="text-blue-600 hover:underline flex items-center gap-1"
>
{selectedForm.data.email}
<ExternalLink className="h-3 w-3" />
</a>
</div>
{selectedForm.data.phone && (
<div className="flex items-center gap-2">
<Phone className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Phone:</span>
<a
href={`tel:${selectedForm.data.phone}`}
className="text-blue-600 hover:underline"
>
{selectedForm.data.phone}
</a>
</div>
)}
{/* Show business info for partnership applications */}
{selectedForm.formId === 'partnership_application' && selectedForm.data.businessName && (
<div className="flex items-center gap-2">
<Building className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Business:</span>
<span>{selectedForm.data.businessName}</span>
</div>
)}
{/* Show company info for other forms */}
{selectedForm.formId !== 'partnership_application' && selectedForm.data.company && (
<div className="flex items-center gap-2">
<Building className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Company:</span>
<span>{selectedForm.data.company}</span>
</div>
)}
</CardContent>
</Card>
{/* Message/Details Content */}
{selectedForm.formId === 'partnership_application' ? (
// Partnership Application Details
<>
<Card>
<CardHeader>
<CardTitle className="text-lg">Partnership Details</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="font-medium">Partnership Tier:</span>
<Badge variant="outline" className="ml-2">{selectedForm.data.partnershipTier}</Badge>
</div>
<div>
<span className="font-medium">Expected Customers:</span>
<span className="ml-2">{selectedForm.data.expectedCustomers || 'Not specified'}</span>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<span className="font-medium">Business Type:</span>
<span className="ml-2">{selectedForm.data.businessType || 'Not specified'}</span>
</div>
<div>
<span className="font-medium">Experience:</span>
<span className="ml-2">{selectedForm.data.experience || 'Not specified'}</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">Address Information</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div><span className="font-medium">Address:</span> {selectedForm.data.address || 'Not provided'}</div>
<div className="grid grid-cols-3 gap-4">
<div><span className="font-medium">City:</span> {selectedForm.data.city || 'Not provided'}</div>
<div><span className="font-medium">State:</span> {selectedForm.data.state || 'Not provided'}</div>
<div><span className="font-medium">Zip Code:</span> {selectedForm.data.zipCode || 'Not provided'}</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">Motivation & Plans</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<span className="font-medium">Motivation:</span>
<div className="bg-muted/50 p-3 rounded-lg mt-2">
<p className="whitespace-pre-wrap">{selectedForm.data.motivation || 'Not provided'}</p>
</div>
</div>
<div>
<span className="font-medium">Marketing Plan:</span>
<div className="bg-muted/50 p-3 rounded-lg mt-2">
<p className="whitespace-pre-wrap">{selectedForm.data.marketingPlan || 'Not provided'}</p>
</div>
</div>
</CardContent>
</Card>
</>
) : (
// Regular Contact Form Message
<Card>
<CardHeader>
<CardTitle className="text-lg">Message</CardTitle>
{selectedForm.data.subject && (
<CardDescription className="font-medium">
Subject: {selectedForm.data.subject}
</CardDescription>
)}
</CardHeader>
<CardContent>
<div className="bg-muted/50 p-4 rounded-lg">
<p className="whitespace-pre-wrap">{selectedForm.data.message}</p>
</div>
</CardContent>
</Card>
)}
{/* Additional Information */}
{(selectedForm.data.inquiryType || selectedForm.data.formSource) && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Additional Information</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{selectedForm.data.inquiryType && (
<div className="flex items-center gap-2">
<span className="font-medium">Inquiry Type:</span>
<Badge variant="outline">{selectedForm.data.inquiryType}</Badge>
</div>
)}
{selectedForm.data.formSource && (
<div className="flex items-center gap-2">
<span className="font-medium">Form Source:</span>
<span>{selectedForm.data.formSource}</span>
</div>
)}
</CardContent>
</Card>
)}
{/* Metadata */}
{selectedForm.metadata && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Technical Details</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
{selectedForm.metadata.ipAddress && (
<div>
<span className="font-medium">IP Address:</span> {selectedForm.metadata.ipAddress}
</div>
)}
{selectedForm.metadata.referrer && (
<div>
<span className="font-medium">Referrer:</span> {selectedForm.metadata.referrer}
</div>
)}
{selectedForm.metadata.userAgent && (
<div>
<span className="font-medium">User Agent:</span>
<div className="mt-1 p-2 bg-muted/50 rounded text-xs break-all">
{selectedForm.metadata.userAgent}
</div>
</div>
)}
</CardContent>
</Card>
)}
{/* Actions */}
<div className="flex gap-3 pt-4 border-t">
<Button
onClick={() => window.open(`mailto:${selectedForm.data.email}?subject=Re: ${selectedForm.data.subject || 'Your inquiry'}`)}
className="flex items-center gap-2"
>
<Mail className="h-4 w-4" />
Reply via Email
</Button>
<Button
variant="outline"
onClick={() => updateFormStatus(selectedForm.id, 'resolved')}
disabled={selectedForm.status === 'resolved'}
>
Mark as Resolved
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
)
}

28
app/admin/layout.tsx Normal file
View File

@@ -0,0 +1,28 @@
import { redirect } from 'next/navigation'
import { auth } from '@/auth'
import { AdminSidebar } from '@/components/admin/AdminSidebar'
import { AdminHeader } from '@/components/admin/AdminHeader'
export default async function AdminLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
redirect('/auth/signin?callbackUrl=/admin')
}
return (
<div className="min-h-screen bg-gray-50">
<AdminHeader user={session.user} />
<div className="flex">
<AdminSidebar />
<main className="flex-1 p-6">
{children}
</main>
</div>
</div>
)
}

135
app/admin/loading.tsx Normal file
View File

@@ -0,0 +1,135 @@
'use client'
import { motion } from 'framer-motion'
import { Settings, Shield, BarChart3 } from 'lucide-react'
export default function AdminLoading() {
return (
<div className="min-h-[70vh] flex items-center justify-center px-4">
<div className="text-center">
{/* Admin Loading Animation */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
className="mb-6"
>
<div className="relative mx-auto w-24 h-24 mb-4">
{/* Outer ring */}
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 3,
repeat: Infinity,
ease: "linear"
}}
className="absolute inset-0 border-3 border-blue-200 border-t-blue-600 rounded-full"
/>
{/* Inner ring */}
<motion.div
animate={{ rotate: -360 }}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
className="absolute inset-3 border-2 border-purple-200 border-b-purple-500 rounded-full"
/>
{/* Center icon */}
<motion.div
animate={{
scale: [1, 1.15, 1],
rotate: [0, 180, 360]
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut"
}}
className="absolute inset-6 flex items-center justify-center bg-gradient-to-br from-blue-50 to-purple-50 rounded-full shadow-sm"
>
<Settings className="w-6 h-6 text-blue-600" />
</motion.div>
</div>
</motion.div>
{/* Loading Text */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="mb-4"
>
<div className="flex items-center justify-center space-x-2 text-blue-600 font-semibold text-lg">
<Shield className="w-5 h-5" />
<span>Loading Admin Panel</span>
</div>
<p className="text-slate-500 text-sm mt-1">
Preparing dashboard data...
</p>
</motion.div>
{/* Progress indicators */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="flex justify-center space-x-2"
>
{[0, 1, 2, 3].map((index) => (
<motion.div
key={index}
className="w-2 h-2 bg-blue-500 rounded-full"
animate={{
scale: [1, 1.4, 1],
opacity: [0.3, 1, 0.3]
}}
transition={{
duration: 1.8,
repeat: Infinity,
delay: index * 0.2,
ease: "easeInOut"
}}
/>
))}
</motion.div>
{/* Stats preview */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.6 }}
className="mt-8 grid grid-cols-3 gap-4 max-w-sm mx-auto"
>
{[
{ icon: BarChart3, label: 'Analytics' },
{ icon: Settings, label: 'Settings' },
{ icon: Shield, label: 'Security' }
].map((item, index) => {
const IconComponent = item.icon
return (
<motion.div
key={item.label}
animate={{
opacity: [0.3, 1, 0.3]
}}
transition={{
duration: 2,
repeat: Infinity,
delay: index * 0.3,
ease: "easeInOut"
}}
className="text-center p-3 bg-white rounded-lg shadow-sm border border-slate-100"
>
<IconComponent className="w-6 h-6 text-slate-400 mx-auto mb-1" />
<span className="text-xs text-slate-500">{item.label}</span>
</motion.div>
)
})}
</motion.div>
</div>
</div>
)
}

View File

@@ -0,0 +1,459 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import {
ArrowLeft,
Package,
User,
CreditCard,
Truck,
CheckCircle,
XCircle,
Clock,
MapPin,
Phone,
Mail
} from 'lucide-react'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { toast } from 'sonner'
import { motion } from 'framer-motion'
import Link from 'next/link'
import Image from 'next/image'
interface OrderItem {
id: string
quantity: number
price: number
product: {
id: string
name: string
images: string[]
sku: string
price: number
}
}
interface ShippingAddress {
firstName: string
lastName: string
company?: string | null
address1: string
address2?: string | null
city: string
state: string
zipCode: string
country: string
phone?: string | null
}
interface Order {
id: string
total: number
status: 'PENDING' | 'PAID' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED'
createdAt: string
updatedAt: string
razorpayOrderId: string | null
razorpayPaymentId: string | null
shippingAddress: ShippingAddress | null
user: {
id: string
name: string
email: string
phone: string | null
}
orderItems: OrderItem[]
}
const statusColors = {
PENDING: 'bg-yellow-100 text-yellow-800 border-yellow-200',
PAID: 'bg-blue-100 text-blue-800 border-blue-200',
SHIPPED: 'bg-purple-100 text-purple-800 border-purple-200',
DELIVERED: 'bg-green-100 text-green-800 border-green-200',
CANCELLED: 'bg-red-100 text-red-800 border-red-200'
}
const statusIcons = {
PENDING: Clock,
PAID: CreditCard,
SHIPPED: Truck,
DELIVERED: CheckCircle,
CANCELLED: XCircle
}
export default function AdminOrderDetail() {
const params = useParams()
const router = useRouter()
const [order, setOrder] = useState<Order | null>(null)
const [loading, setLoading] = useState(true)
const [updating, setUpdating] = useState(false)
const orderId = params.id as string
const fetchOrder = useCallback(async () => {
try {
setLoading(true)
const response = await fetch(`/api/admin/orders/${orderId}`)
if (!response.ok) {
throw new Error('Failed to fetch order')
}
const data = await response.json()
setOrder(data)
} catch (error) {
console.error('Error fetching order:', error)
toast.error('Failed to load order details')
} finally {
setLoading(false)
}
}, [orderId])
const updateOrderStatus = async (newStatus: string) => {
try {
setUpdating(true)
const response = await fetch(`/api/admin/orders/${orderId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: newStatus }),
})
if (!response.ok) {
throw new Error('Failed to update order status')
}
const data = await response.json()
setOrder(data)
toast.success('Order status updated successfully')
} catch (error) {
console.error('Error updating order:', error)
toast.error('Failed to update order status')
} finally {
setUpdating(false)
}
}
useEffect(() => {
if (orderId) {
fetchOrder()
}
}, [orderId, fetchOrder])
if (loading) {
return (
<div className="container mx-auto px-4 py-8">
<div className="animate-pulse space-y-6">
<div className="h-8 bg-gray-200 rounded w-1/4"></div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-4">
<div className="h-96 bg-gray-200 rounded"></div>
</div>
<div className="space-y-4">
<div className="h-48 bg-gray-200 rounded"></div>
<div className="h-32 bg-gray-200 rounded"></div>
</div>
</div>
</div>
</div>
)
}
if (!order) {
return (
<div className="container mx-auto px-4 py-8">
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Package className="h-12 w-12 text-gray-400 mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">Order Not Found</h3>
<p className="text-gray-600 mb-4">The order you're looking for doesn't exist.</p>
<Link href="/admin/orders">
<Button>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Orders
</Button>
</Link>
</CardContent>
</Card>
</div>
)
}
const StatusIcon = statusIcons[order.status] || Clock
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="container mx-auto px-4 py-8"
>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<Link href="/admin/orders">
<Button variant="outline" size="sm">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Orders
</Button>
</Link>
<div>
<h1 className="text-2xl font-bold text-gray-900">Order #{order.id}</h1>
<p className="text-gray-600">
Placed on {new Date(order.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
</div>
<Badge className={statusColors[order.status]}>
<StatusIcon className="h-4 w-4 mr-1" />
{order.status}
</Badge>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Order Items */}
<div className="lg:col-span-2">
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<Package className="h-5 w-5 mr-2" />
Order Items ({order.orderItems?.length || 0})
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{order.orderItems && order.orderItems.length > 0 ? (
order.orderItems.map((item) => (
<div key={item.id} className="flex items-center space-x-4 p-4 border rounded-lg bg-gray-50">
<div className="w-16 h-16 bg-gray-200 rounded-lg flex items-center justify-center">
{item.product?.images && item.product.images.length > 0 ? (
<Image
src={item.product.images[0]}
alt={item.product.name || 'Product'}
width={64}
height={64}
className="w-full h-full object-cover rounded-lg"
/>
) : (
<Package className="h-8 w-8 text-gray-400" />
)}
</div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900">{item.product?.name || 'Unknown Product'}</h4>
<p className="text-sm text-gray-600">SKU: {item.product?.sku || 'N/A'}</p>
<div className="flex items-center space-x-4 mt-1">
<span className="text-sm text-gray-600">Qty: {item.quantity}</span>
<span className="text-sm text-gray-600">Price: {item.price.toFixed(2)} each</span>
</div>
</div>
<div className="text-right">
<p className="font-semibold text-gray-900">
{(item.price * item.quantity).toFixed(2)}
</p>
<p className="text-xs text-gray-500">
{item.quantity} × {item.price.toFixed(2)}
</p>
</div>
</div>
))
) : (
<div className="text-center py-8">
<Package className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">No items found in this order</p>
</div>
)}
</div>
<Separator className="my-4" />
<div className="flex justify-between items-center">
<span className="text-lg font-semibold">Total Amount:</span>
<span className="text-xl font-bold text-green-600">{order.total?.toFixed(2) || '0.00'}</span>
</div>
</CardContent>
</Card>
</div>
{/* Order Details Sidebar */}
<div className="space-y-6">
{/* Order Actions */}
<Card>
<CardHeader>
<CardTitle>Order Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div>
<label className="text-sm font-medium text-gray-700 mb-2 block">
Update Order Status
</label>
<Select
value={order.status}
onValueChange={(value) => updateOrderStatus(value)}
disabled={updating}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="PENDING">
<div className="flex items-center">
<Clock className="h-4 w-4 mr-2 text-yellow-600" />
Pending
</div>
</SelectItem>
<SelectItem value="PAID">
<div className="flex items-center">
<CreditCard className="h-4 w-4 mr-2 text-blue-600" />
Paid
</div>
</SelectItem>
<SelectItem value="SHIPPED">
<div className="flex items-center">
<Truck className="h-4 w-4 mr-2 text-purple-600" />
Shipped
</div>
</SelectItem>
<SelectItem value="DELIVERED">
<div className="flex items-center">
<CheckCircle className="h-4 w-4 mr-2 text-green-600" />
Delivered
</div>
</SelectItem>
<SelectItem value="CANCELLED">
<div className="flex items-center">
<XCircle className="h-4 w-4 mr-2 text-red-600" />
Cancelled
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{updating && (
<div className="text-sm text-gray-500 flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 mr-2"></div>
Updating order status...
</div>
)}
</CardContent>
</Card>
{/* Customer Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<User className="h-5 w-5 mr-2" />
Customer Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center space-x-3">
<User className="h-4 w-4 text-gray-500" />
<span>{order.user?.name || 'Unknown User'}</span>
</div>
<div className="flex items-center space-x-3">
<Mail className="h-4 w-4 text-gray-500" />
<span className="text-sm">{order.user?.email || 'No email'}</span>
</div>
{order.user?.phone && (
<div className="flex items-center space-x-3">
<Phone className="h-4 w-4 text-gray-500" />
<span>{order.user.phone}</span>
</div>
)}
</CardContent>
</Card>
{/* Shipping Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<MapPin className="h-5 w-5 mr-2" />
Shipping Address
</CardTitle>
</CardHeader>
<CardContent>
{order.shippingAddress ? (
<div className="space-y-2">
<p className="font-semibold">
{order.shippingAddress.firstName} {order.shippingAddress.lastName}
</p>
{order.shippingAddress.company && (
<p className="text-sm text-gray-600">{order.shippingAddress.company}</p>
)}
<p className="text-sm">{order.shippingAddress.address1}</p>
{order.shippingAddress.address2 && (
<p className="text-sm">{order.shippingAddress.address2}</p>
)}
<p className="text-sm">
{order.shippingAddress.city}, {order.shippingAddress.state} {order.shippingAddress.zipCode}
</p>
<p className="text-sm">{order.shippingAddress.country}</p>
{order.shippingAddress.phone && (
<p className="text-sm flex items-center mt-2">
<Phone className="h-4 w-4 mr-1" />
{order.shippingAddress.phone}
</p>
)}
</div>
) : (
<p className="text-sm text-gray-500">No shipping address provided</p>
)}
</CardContent>
</Card>
{/* Payment Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<CreditCard className="h-5 w-5 mr-2" />
Payment Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div>
<span className="text-sm font-medium text-gray-500">Payment Status:</span>
<Badge className={`ml-2 ${statusColors[order.status]}`}>
{order.status}
</Badge>
</div>
{order.razorpayOrderId && (
<div>
<span className="text-sm font-medium text-gray-500">Razorpay Order ID:</span>
<p className="text-sm font-mono bg-gray-50 p-2 rounded mt-1">{order.razorpayOrderId}</p>
</div>
)}
{order.razorpayPaymentId && (
<div>
<span className="text-sm font-medium text-gray-500">Payment ID:</span>
<p className="text-sm font-mono bg-gray-50 p-2 rounded mt-1">{order.razorpayPaymentId}</p>
</div>
)}
<Separator />
<div>
<span className="text-sm font-medium text-gray-500">Total Amount:</span>
<p className="text-lg font-bold text-green-600">{order.total?.toFixed(2) || '0.00'}</p>
</div>
</CardContent>
</Card>
</div>
</div>
</motion.div>
)
}

484
app/admin/orders/page.tsx Normal file
View File

@@ -0,0 +1,484 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Search, Download, MoreHorizontal, Eye, Package, Truck, CheckCircle, XCircle, Clock, CreditCard, Filter } from 'lucide-react'
import { toast } from 'sonner'
import Link from 'next/link'
import { motion } from 'framer-motion'
interface Order {
id: string
total: number
status: 'PENDING' | 'PAID' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED'
createdAt: string
razorpayOrderId: string | null
razorpayPaymentId: string | null
user: {
id: string
name: string
email: string
}
orderItems: {
id: string
quantity: number
price: number
product: {
id: string
name: string
images: string[]
}
}[]
}
const statusConfig = {
PENDING: {
label: 'Pending',
color: 'bg-gradient-to-r from-yellow-400 to-orange-500',
icon: Clock,
description: 'Awaiting payment'
},
PAID: {
label: 'Paid',
color: 'bg-gradient-to-r from-blue-500 to-indigo-600',
icon: CreditCard,
description: 'Payment confirmed'
},
SHIPPED: {
label: 'Shipped',
color: 'bg-gradient-to-r from-purple-500 to-pink-600',
icon: Truck,
description: 'Order shipped'
},
DELIVERED: {
label: 'Delivered',
color: 'bg-gradient-to-r from-green-500 to-emerald-600',
icon: CheckCircle,
description: 'Successfully delivered'
},
CANCELLED: {
label: 'Cancelled',
color: 'bg-gradient-to-r from-red-500 to-pink-600',
icon: XCircle,
description: 'Order cancelled'
}
}
export default function AdminOrdersPage() {
const [orders, setOrders] = useState<Order[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState('')
const [statusFilter, setStatusFilter] = useState('all')
const [selectedOrders, setSelectedOrders] = useState<string[]>([])
const [bulkActionLoading, setBulkActionLoading] = useState(false)
const fetchOrders = useCallback(async () => {
try {
setLoading(true)
const params = new URLSearchParams()
if (search) params.append('search', search)
if (statusFilter && statusFilter !== 'all') params.append('status', statusFilter)
const response = await fetch(`/api/admin/orders?${params}`)
const data = await response.json()
setOrders(data || [])
} catch (error) {
console.error('Error fetching orders:', error)
toast.error('Failed to load orders')
} finally {
setLoading(false)
}
}, [search, statusFilter])
useEffect(() => {
fetchOrders()
}, [fetchOrders])
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedOrders(orders.map(o => o.id))
} else {
setSelectedOrders([])
}
}
const handleSelectOrder = (orderId: string, checked: boolean) => {
if (checked) {
setSelectedOrders(prev => [...prev, orderId])
} else {
setSelectedOrders(prev => prev.filter(id => id !== orderId))
}
}
const handleBulkStatusUpdate = async (newStatus: string) => {
if (selectedOrders.length === 0) {
toast.error('Please select at least one order')
return
}
setBulkActionLoading(true)
try {
const response = await fetch('/api/admin/orders/bulk', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orderIds: selectedOrders,
status: newStatus
})
})
if (!response.ok) throw new Error('Bulk update failed')
toast.success(`Successfully updated ${selectedOrders.length} orders`)
setSelectedOrders([])
fetchOrders()
} catch (error) {
toast.error('Failed to update orders')
} finally {
setBulkActionLoading(false)
}
}
const handleStatusUpdate = async (orderId: string, newStatus: string) => {
try {
const response = await fetch(`/api/admin/orders/${orderId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus })
})
if (!response.ok) throw new Error('Status update failed')
toast.success('Order status updated successfully')
fetchOrders()
} catch (error) {
toast.error('Failed to update order status')
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100 p-6">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6"
>
<div>
<h1 className="text-4xl font-bold bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent">
Orders Management
</h1>
<p className="text-gray-600 mt-2">Manage customer orders and track shipments</p>
</div>
<div className="flex items-center space-x-3">
<Button variant="outline" size="sm" className="bg-white shadow-md hover:shadow-lg transition-shadow">
<Download className="h-4 w-4 mr-2" />
Export Data
</Button>
<Button className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 shadow-lg">
<Filter className="h-4 w-4 mr-2" />
Advanced Filters
</Button>
</div>
</motion.div>
{/* Stats Cards */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6"
>
{Object.entries(statusConfig).map(([status, config], index) => {
const StatusIcon = config.icon
const count = orders.filter(order => order.status === status).length
return (
<Card key={status} className="bg-white/80 backdrop-blur-sm border-0 shadow-lg hover:shadow-xl transition-all duration-300">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">{config.label}</p>
<p className="text-2xl font-bold text-gray-900">{count}</p>
</div>
<div className={`w-12 h-12 ${config.color} rounded-xl flex items-center justify-center shadow-lg`}>
<StatusIcon className="h-6 w-6 text-white" />
</div>
</div>
<p className="text-xs text-gray-500 mt-2">{config.description}</p>
</CardContent>
</Card>
)
})}
</motion.div>
{/* Main Orders Table */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<Card className="bg-white/90 backdrop-blur-sm border-0 shadow-xl">
<CardHeader className="pb-4">
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
<CardTitle className="text-2xl font-bold">All Orders</CardTitle>
<div className="flex flex-wrap items-center gap-3">
{selectedOrders.length > 0 && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="flex items-center space-x-3 bg-blue-50 px-4 py-2 rounded-lg border border-blue-200"
>
<span className="text-sm font-medium text-blue-700">
{selectedOrders.length} selected
</span>
<Select onValueChange={handleBulkStatusUpdate}>
<SelectTrigger className="w-40 bg-white">
<SelectValue placeholder="Update Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="PAID">Mark as Paid</SelectItem>
<SelectItem value="SHIPPED">Mark as Shipped</SelectItem>
<SelectItem value="DELIVERED">Mark as Delivered</SelectItem>
<SelectItem value="CANCELLED">Mark as Cancelled</SelectItem>
</SelectContent>
</Select>
</motion.div>
)}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search orders, customers..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 w-64 bg-white shadow-sm"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-40 bg-white shadow-sm">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="PENDING">Pending</SelectItem>
<SelectItem value="PAID">Paid</SelectItem>
<SelectItem value="SHIPPED">Shipped</SelectItem>
<SelectItem value="DELIVERED">Delivered</SelectItem>
<SelectItem value="CANCELLED">Cancelled</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="relative">
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
<div className="w-10 h-10 border-4 border-gray-100 border-t-purple-500 rounded-full animate-spin absolute top-1 left-1"></div>
</div>
</div>
) : (
<div className="overflow-hidden rounded-lg border border-gray-200">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={selectedOrders.length === orders.length && orders.length > 0}
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead className="font-semibold">Order</TableHead>
<TableHead className="font-semibold">Customer</TableHead>
<TableHead className="font-semibold">Items</TableHead>
<TableHead className="font-semibold">Total</TableHead>
<TableHead className="font-semibold">Status</TableHead>
<TableHead className="font-semibold">Date</TableHead>
<TableHead className="font-semibold">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{orders.map((order, index) => {
const StatusIcon = statusConfig[order.status].icon
return (
<motion.tr
key={order.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className="hover:bg-gray-50 transition-colors"
>
<TableCell>
<Checkbox
checked={selectedOrders.includes(order.id)}
onCheckedChange={(checked) => handleSelectOrder(order.id, !!checked)}
/>
</TableCell>
<TableCell>
<div className="space-y-1">
<p className="font-semibold text-gray-900">#{order.id.slice(-8)}</p>
{order.razorpayOrderId && (
<p className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
Razorpay: {order.razorpayOrderId.slice(-8)}
</p>
)}
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-r from-blue-500 to-purple-600 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-medium">
{order.user.name?.charAt(0) || 'U'}
</span>
</div>
<div>
<p className="font-medium text-gray-900">{order.user.name}</p>
<p className="text-sm text-gray-500">{order.user.email}</p>
</div>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
{order.orderItems.slice(0, 2).map((item) => (
<div key={item.id} className="text-sm">
<span className="font-medium">{item.quantity}x</span>{' '}
<span className="text-gray-600">{item.product.name}</span>
</div>
))}
{order.orderItems.length > 2 && (
<p className="text-xs text-blue-600 font-medium">
+{order.orderItems.length - 2} more items
</p>
)}
</div>
</TableCell>
<TableCell>
<span className="font-bold text-lg bg-gradient-to-r from-green-600 to-emerald-600 bg-clip-text text-transparent">
{order.total.toFixed(2)}
</span>
</TableCell>
<TableCell>
<Badge className={`${statusConfig[order.status].color} text-white shadow-md`}>
<StatusIcon className="h-3 w-3 mr-1" />
{statusConfig[order.status].label}
</Badge>
</TableCell>
<TableCell>
<div className="text-sm">
<p className="font-medium text-gray-900">
{new Date(order.createdAt).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
})}
</p>
<p className="text-gray-500">
{new Date(order.createdAt).toLocaleDateString('en-US', {
year: 'numeric'
})}
</p>
</div>
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="hover:bg-gray-100">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem asChild>
<Link href={`/admin/orders/${order.id}`} className="flex items-center">
<Eye className="h-4 w-4 mr-2" />
View Details
</Link>
</DropdownMenuItem>
{order.status === 'PENDING' && (
<DropdownMenuItem
onClick={() => handleStatusUpdate(order.id, 'PAID')}
className="text-blue-600"
>
<CreditCard className="h-4 w-4 mr-2" />
Mark as Paid
</DropdownMenuItem>
)}
{order.status === 'PAID' && (
<DropdownMenuItem
onClick={() => handleStatusUpdate(order.id, 'SHIPPED')}
className="text-purple-600"
>
<Truck className="h-4 w-4 mr-2" />
Mark as Shipped
</DropdownMenuItem>
)}
{order.status === 'SHIPPED' && (
<DropdownMenuItem
onClick={() => handleStatusUpdate(order.id, 'DELIVERED')}
className="text-green-600"
>
<Package className="h-4 w-4 mr-2" />
Mark as Delivered
</DropdownMenuItem>
)}
{['PENDING', 'PAID'].includes(order.status) && (
<DropdownMenuItem
onClick={() => handleStatusUpdate(order.id, 'CANCELLED')}
className="text-red-600"
>
<XCircle className="h-4 w-4 mr-2" />
Cancel Order
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</motion.tr>
)
})}
</TableBody>
</Table>
</div>
)}
{!loading && orders.length === 0 && (
<div className="text-center py-12">
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Package className="h-10 w-10 text-gray-400" />
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No orders found</h3>
<p className="text-gray-500">Try adjusting your search or filter criteria</p>
</div>
)}
</CardContent>
</Card>
</motion.div>
</div>
</div>
)
}

253
app/admin/page.tsx Normal file
View File

@@ -0,0 +1,253 @@
'use client'
import { useEffect, useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Users, Package, ShoppingCart, DollarSign, TrendingUp, TrendingDown } from 'lucide-react'
import { motion } from 'framer-motion'
import { Badge } from '@/components/ui/badge'
interface DashboardStats {
totalUsers: number
totalProducts: number
totalOrders: number
totalRevenue: number
userGrowth: number
productGrowth: number
orderGrowth: number
revenueGrowth: number
}
interface RecentOrder {
id: string
total: number
status: string
createdAt: string
user: {
name: string
email: string
}
}
interface TopProduct {
id: string
name: string
price: number
orderCount: number
totalRevenue: number
}
export default function AdminDashboard() {
const [stats, setStats] = useState<DashboardStats | null>(null)
const [recentOrders, setRecentOrders] = useState<RecentOrder[]>([])
const [topProducts, setTopProducts] = useState<TopProduct[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchDashboardData()
}, [])
const fetchDashboardData = async () => {
try {
const [statsRes, ordersRes, productsRes] = await Promise.all([
fetch('/api/admin/dashboard/stats'),
fetch('/api/admin/dashboard/recent-orders'),
fetch('/api/admin/dashboard/top-products')
])
const [statsData, ordersData, productsData] = await Promise.all([
statsRes.json(),
ordersRes.json(),
productsRes.json()
])
setStats(statsData)
setRecentOrders(ordersData.orders || [])
setTopProducts(productsData.products || [])
} catch (error) {
console.error('Error fetching dashboard data:', error)
} finally {
setLoading(false)
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'PENDING': return 'bg-yellow-100 text-yellow-800'
case 'PAID': return 'bg-blue-100 text-blue-800'
case 'SHIPPED': return 'bg-purple-100 text-purple-800'
case 'DELIVERED': return 'bg-green-100 text-green-800'
case 'CANCELLED': return 'bg-red-100 text-red-800'
default: return 'bg-gray-100 text-gray-800'
}
}
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
</div>
)
}
const dashboardStats = stats ? [
{
title: 'Total Users',
value: stats.totalUsers.toLocaleString(),
change: `${stats.userGrowth >= 0 ? '+' : ''}${stats.userGrowth.toFixed(1)}%`,
trend: stats.userGrowth >= 0 ? 'up' : 'down',
icon: Users,
},
{
title: 'Products',
value: stats.totalProducts.toLocaleString(),
change: `${stats.productGrowth >= 0 ? '+' : ''}${stats.productGrowth.toFixed(1)}%`,
trend: stats.productGrowth >= 0 ? 'up' : 'down',
icon: Package,
},
{
title: 'Orders',
value: stats.totalOrders.toLocaleString(),
change: `${stats.orderGrowth >= 0 ? '+' : ''}${stats.orderGrowth.toFixed(1)}%`,
trend: stats.orderGrowth >= 0 ? 'up' : 'down',
icon: ShoppingCart,
},
{
title: 'Revenue',
value: `${stats.totalRevenue.toLocaleString()}`,
change: `${stats.revenueGrowth >= 0 ? '+' : ''}${stats.revenueGrowth.toFixed(1)}%`,
trend: stats.revenueGrowth >= 0 ? 'up' : 'down',
icon: DollarSign,
},
] : []
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
<div>
<h1 className="text-3xl font-bold text-gray-900">Admin Dashboard</h1>
<p className="text-gray-600 mt-2">Welcome to your admin dashboard</p>
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{dashboardStats.map((stat, index) => (
<motion.div
key={stat.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
<Card className="bg-white/80 backdrop-blur-sm shadow-lg border-0">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-600">
{stat.title}
</CardTitle>
<stat.icon className="h-5 w-5 text-gray-400" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-900">{stat.value}</div>
<p className="text-xs text-muted-foreground flex items-center mt-2">
{stat.trend === 'up' ? (
<TrendingUp className="h-3 w-3 text-green-500 mr-1" />
) : (
<TrendingDown className="h-3 w-3 text-red-500 mr-1" />
)}
<span className={stat.trend === 'up' ? 'text-green-600 font-medium' : 'text-red-600 font-medium'}>
{stat.change}
</span>
<span className="ml-1 text-gray-500">from last month</span>
</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
{/* Recent Orders */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4 }}
>
<Card className="bg-white/80 backdrop-blur-sm shadow-lg border-0">
<CardHeader>
<CardTitle>Recent Orders</CardTitle>
<CardDescription>Latest orders from your store</CardDescription>
</CardHeader>
<CardContent>
{recentOrders.length === 0 ? (
<div className="text-center py-8">
<ShoppingCart className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">No recent orders</p>
</div>
) : (
<div className="space-y-4">
{recentOrders.map((order) => (
<div key={order.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<p className="font-medium">Order #{order.id.slice(-8)}</p>
<Badge className={getStatusColor(order.status)}>
{order.status}
</Badge>
</div>
<p className="text-sm text-gray-500">{order.user.name}</p>
<p className="text-xs text-gray-400">
{new Date(order.createdAt).toLocaleDateString()}
</p>
</div>
<span className="font-bold text-green-600">{order.total.toFixed(2)}</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
</motion.div>
{/* Top Products */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
>
<Card className="bg-white/80 backdrop-blur-sm shadow-lg border-0">
<CardHeader>
<CardTitle>Top Products</CardTitle>
<CardDescription>Best selling products this month</CardDescription>
</CardHeader>
<CardContent>
{topProducts.length === 0 ? (
<div className="text-center py-8">
<Package className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">No product data available</p>
</div>
) : (
<div className="space-y-4">
{topProducts.map((product, index) => (
<div key={product.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<span className="text-sm font-bold text-blue-600">#{index + 1}</span>
</div>
<div>
<p className="font-medium text-gray-900">{product.name}</p>
<p className="text-sm text-gray-500">{product.orderCount} sold</p>
</div>
</div>
<div className="text-right">
<p className="font-bold text-gray-900">{product.price.toFixed(2)}</p>
<p className="text-xs text-green-600">{product.totalRevenue.toFixed(0)} revenue</p>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</motion.div>
</div>
</div>
</div>
)
}

408
app/admin/payouts/page.tsx Normal file
View File

@@ -0,0 +1,408 @@
'use client'
import { useEffect, useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { DollarSign, Clock, CheckCircle, XCircle, Eye, Search, Filter } from 'lucide-react'
import { motion } from 'framer-motion'
import { toast } from 'sonner'
interface Payout {
id: string
amount: number
status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'PAID'
bankDetails: string
adminNotes?: string
createdAt: string
updatedAt: string
user: {
id: string
name: string
email: string
}
}
interface PayoutStats {
totalRequests: number
pendingAmount: number
approvedAmount: number
paidAmount: number
}
export default function AdminPayoutsPage() {
const [payouts, setPayouts] = useState<Payout[]>([])
const [stats, setStats] = useState<PayoutStats | null>(null)
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState({
status: 'all',
search: ''
})
const [selectedPayout, setSelectedPayout] = useState<Payout | null>(null)
const [reviewDialogOpen, setReviewDialogOpen] = useState(false)
const [reviewStatus, setReviewStatus] = useState<'APPROVED' | 'REJECTED'>('APPROVED')
const [adminNotes, setAdminNotes] = useState('')
const [processing, setProcessing] = useState(false)
useEffect(() => {
fetchPayouts()
fetchStats()
}, [])
const fetchPayouts = async () => {
try {
const response = await fetch('/api/admin/payouts')
if (!response.ok) throw new Error('Failed to fetch payouts')
const data = await response.json()
setPayouts(data.payouts || [])
} catch (error) {
console.error('Error fetching payouts:', error)
toast.error('Failed to load payouts')
} finally {
setLoading(false)
}
}
const fetchStats = async () => {
try {
const response = await fetch('/api/admin/payouts/stats')
if (!response.ok) throw new Error('Failed to fetch stats')
const data = await response.json()
setStats(data)
} catch (error) {
console.error('Error fetching stats:', error)
}
}
const handleReviewPayout = async () => {
if (!selectedPayout) return
setProcessing(true)
try {
const response = await fetch(`/api/admin/payouts/${selectedPayout.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
status: reviewStatus,
adminNotes
})
})
if (!response.ok) throw new Error('Failed to update payout')
toast.success(`Payout ${reviewStatus.toLowerCase()} successfully`)
setReviewDialogOpen(false)
setSelectedPayout(null)
setAdminNotes('')
fetchPayouts()
fetchStats()
} catch (error) {
console.error('Error updating payout:', error)
toast.error('Failed to update payout')
} finally {
setProcessing(false)
}
}
const openReviewDialog = (payout: Payout) => {
setSelectedPayout(payout)
setAdminNotes(payout.adminNotes || '')
setReviewDialogOpen(true)
}
const getStatusColor = (status: string) => {
switch (status) {
case 'PENDING': return 'bg-yellow-100 text-yellow-800'
case 'APPROVED': return 'bg-blue-100 text-blue-800'
case 'REJECTED': return 'bg-red-100 text-red-800'
case 'PAID': return 'bg-green-100 text-green-800'
default: return 'bg-gray-100 text-gray-800'
}
}
const getStatusIcon = (status: string) => {
switch (status) {
case 'PENDING': return <Clock className="h-4 w-4 text-yellow-500" />
case 'APPROVED': return <CheckCircle className="h-4 w-4 text-blue-500" />
case 'REJECTED': return <XCircle className="h-4 w-4 text-red-500" />
case 'PAID': return <CheckCircle className="h-4 w-4 text-green-500" />
default: return <Clock className="h-4 w-4 text-gray-500" />
}
}
const filteredPayouts = payouts.filter(payout => {
const matchesStatus = filter.status === 'all' || payout.status === filter.status
const matchesSearch = !filter.search ||
payout.user.name.toLowerCase().includes(filter.search.toLowerCase()) ||
payout.user.email.toLowerCase().includes(filter.search.toLowerCase())
return matchesStatus && matchesSearch
})
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Payout Management</h1>
<p className="text-gray-600 mt-2">Review and manage user payout requests</p>
</div>
</div>
{/* Stats Cards */}
{stats && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card className="bg-gradient-to-r from-blue-500 to-blue-600 text-white">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-sm">Total Requests</p>
<p className="text-2xl font-bold">{stats.totalRequests}</p>
</div>
<DollarSign className="h-8 w-8 text-blue-200" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-yellow-500 to-yellow-600 text-white">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-yellow-100 text-sm">Pending Amount</p>
<p className="text-2xl font-bold">{stats.pendingAmount.toFixed(0)}</p>
</div>
<Clock className="h-8 w-8 text-yellow-200" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-purple-500 to-purple-600 text-white">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-purple-100 text-sm">Approved Amount</p>
<p className="text-2xl font-bold">{stats.approvedAmount.toFixed(0)}</p>
</div>
<CheckCircle className="h-8 w-8 text-purple-200" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-r from-green-500 to-green-600 text-white">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-green-100 text-sm">Paid Amount</p>
<p className="text-2xl font-bold">{stats.paidAmount.toFixed(0)}</p>
</div>
<CheckCircle className="h-8 w-8 text-green-200" />
</div>
</CardContent>
</Card>
</div>
)}
{/* Filters */}
<Card>
<CardHeader>
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<CardTitle>Payout Requests</CardTitle>
<div className="flex items-center space-x-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search users..."
value={filter.search}
onChange={(e) => setFilter(prev => ({ ...prev, search: e.target.value }))}
className="pl-10 w-64"
/>
</div>
<Select value={filter.status} onValueChange={(value) => setFilter(prev => ({ ...prev, status: value }))}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="PENDING">Pending</SelectItem>
<SelectItem value="APPROVED">Approved</SelectItem>
<SelectItem value="REJECTED">Rejected</SelectItem>
<SelectItem value="PAID">Paid</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
{filteredPayouts.length === 0 ? (
<div className="text-center py-8">
<DollarSign className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No payout requests found</h3>
<p className="text-gray-500">Payout requests will appear here when users submit them</p>
</div>
) : (
<div className="space-y-4">
{filteredPayouts.map((payout, index) => (
<motion.div
key={payout.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className="flex items-center justify-between p-4 border border-gray-200 rounded-lg hover:shadow-md transition-shadow"
>
<div className="flex items-center space-x-4">
{getStatusIcon(payout.status)}
<div>
<div className="flex items-center space-x-2 mb-1">
<span className="font-medium text-gray-900">{payout.user.name}</span>
<Badge className={getStatusColor(payout.status)}>
{payout.status}
</Badge>
</div>
<p className="text-sm text-gray-500">{payout.user.email}</p>
<p className="text-xs text-gray-400">
Requested {new Date(payout.createdAt).toLocaleDateString()}
</p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="text-right">
<p className="text-lg font-bold text-gray-900">{payout.amount.toFixed(2)}</p>
<p className="text-xs text-gray-500">
Updated {new Date(payout.updatedAt).toLocaleDateString()}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => openReviewDialog(payout)}
disabled={payout.status === 'PAID'}
>
<Eye className="h-4 w-4 mr-1" />
{payout.status === 'PENDING' ? 'Review' : 'View'}
</Button>
</div>
</motion.div>
))}
</div>
)}
</CardContent>
</Card>
{/* Review Dialog */}
<Dialog open={reviewDialogOpen} onOpenChange={setReviewDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Review Payout Request</DialogTitle>
<DialogDescription>
Review and take action on this payout request
</DialogDescription>
</DialogHeader>
{selectedPayout && (
<div className="space-y-6">
{/* User Info */}
<div className="p-4 bg-gray-50 rounded-lg">
<h4 className="font-medium text-gray-900 mb-2">User Information</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-500">Name:</span>
<p className="font-medium">{selectedPayout.user.name}</p>
</div>
<div>
<span className="text-gray-500">Email:</span>
<p className="font-medium">{selectedPayout.user.email}</p>
</div>
<div>
<span className="text-gray-500">Amount:</span>
<p className="font-bold text-lg">{selectedPayout.amount.toFixed(2)}</p>
</div>
<div>
<span className="text-gray-500">Status:</span>
<Badge className={getStatusColor(selectedPayout.status)}>
{selectedPayout.status}
</Badge>
</div>
</div>
</div>
{/* Bank Details */}
<div>
<Label>Bank Details</Label>
<div className="p-3 bg-gray-50 rounded border mt-1">
<pre className="text-sm whitespace-pre-wrap">{selectedPayout.bankDetails}</pre>
</div>
</div>
{/* Action Section */}
{selectedPayout.status === 'PENDING' && (
<div className="space-y-4">
<div>
<Label>Action</Label>
<Select value={reviewStatus} onValueChange={(value: 'APPROVED' | 'REJECTED') => setReviewStatus(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="APPROVED">Approve</SelectItem>
<SelectItem value="REJECTED">Reject</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Admin Notes</Label>
<Textarea
value={adminNotes}
onChange={(e) => setAdminNotes(e.target.value)}
placeholder="Add notes about this decision..."
rows={3}
/>
</div>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={() => setReviewDialogOpen(false)}>
Cancel
</Button>
<Button onClick={handleReviewPayout} disabled={processing}>
{processing ? 'Processing...' : `${reviewStatus === 'APPROVED' ? 'Approve' : 'Reject'} Payout`}
</Button>
</div>
</div>
)}
{/* Existing Notes */}
{selectedPayout.adminNotes && selectedPayout.status !== 'PENDING' && (
<div>
<Label>Admin Notes</Label>
<div className="p-3 bg-gray-50 rounded border mt-1">
<p className="text-sm">{selectedPayout.adminNotes}</p>
</div>
</div>
)}
</div>
)}
</DialogContent>
</Dialog>
</div>
</div>
)
}

View File

@@ -0,0 +1,84 @@
'use client'
import { useState, useEffect } from 'react'
import { useParams } from 'next/navigation'
import { ProductForm } from '@/components/admin/ProductForm'
import { toast } from 'sonner'
interface Product {
id: string
name: string
description: string
price: number
discount: number
images: string[]
stock: number
manageStock: boolean
sku: string
isActive: boolean
categoryId: string
category: {
id: string
name: string
}
}
export default function EditProductPage() {
const params = useParams()
const [product, setProduct] = useState<Product | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
if (params.id) {
fetchProduct(params.id as string)
}
}, [params.id])
const fetchProduct = async (id: string) => {
try {
const response = await fetch(`/api/admin/products/${id}`)
if (!response.ok) throw new Error('Product not found')
const data = await response.json()
// Transform the data to match the form's expected structure
const transformedProduct = {
...data,
categoryId: data.category.id
}
setProduct(transformedProduct)
} catch (error) {
console.error('Error fetching product:', error)
toast.error('Failed to load product')
} finally {
setLoading(false)
}
}
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
</div>
)
}
if (!product) {
return (
<div className="text-center py-12">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Product not found</h2>
<p className="text-gray-600">The product you&apos;re looking for doesn&apos;t exist.</p>
</div>
)
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Edit Product</h1>
<p className="text-gray-600">Update product information</p>
</div>
<ProductForm product={product} />
</div>
)
}

View File

@@ -0,0 +1,14 @@
import { ProductForm } from '@/components/admin/ProductForm'
export default function NewProductPage() {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Add New Product</h1>
<p className="text-gray-600">Create a new product in your catalog</p>
</div>
<ProductForm />
</div>
)
}

653
app/admin/products/page.tsx Normal file
View File

@@ -0,0 +1,653 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import CsvImport from '@/components/ui/csv-import'
import CsvExport from '@/components/ui/csv-export'
import { Search, Plus, Edit, Trash2, MoreHorizontal, Download, Save, Upload } from 'lucide-react'
import { toast } from 'sonner'
import Link from 'next/link'
import Image from 'next/image'
interface Product {
id: string
name: string
description: string | null
price: number
discount: number
stock: number
isActive: boolean
images: string[]
sku: string
weight: string | null
categoryId: string
category: {
id: string
name: string
}
}
interface QuickEditFormData {
name: string
description: string
price: number
discount: number
stock: number
weight: string
}
export default function AdminProductsPage() {
const [products, setProducts] = useState<Product[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState('')
const [selectedProducts, setSelectedProducts] = useState<string[]>([])
const [bulkActionLoading, setBulkActionLoading] = useState(false)
const [quickEditOpen, setQuickEditOpen] = useState(false)
const [editingProduct, setEditingProduct] = useState<Product | null>(null)
const [quickEditForm, setQuickEditForm] = useState<QuickEditFormData>({
name: '',
description: '',
price: 0,
discount: 0,
stock: 0,
weight: ''
})
const [quickEditLoading, setQuickEditLoading] = useState(false)
const fetchProducts = useCallback(async () => {
try {
setLoading(true)
const params = new URLSearchParams({
page: '1',
limit: '20',
admin: 'true' // Add admin flag to get all products
})
if (search) params.append('search', search)
const response = await fetch(`/api/products?${params}`)
const data = await response.json()
setProducts(data.products || [])
} catch (error) {
console.error('Error fetching products:', error)
toast.error('Failed to load products')
} finally {
setLoading(false)
}
}, [search])
useEffect(() => {
fetchProducts()
}, [fetchProducts])
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedProducts(products.map(p => p.id))
} else {
setSelectedProducts([])
}
}
const handleSelectProduct = (productId: string, checked: boolean) => {
if (checked) {
setSelectedProducts(prev => [...prev, productId])
} else {
setSelectedProducts(prev => prev.filter(id => id !== productId))
}
}
const handleBulkAction = async (action: 'activate' | 'deactivate' | 'delete') => {
if (selectedProducts.length === 0) {
toast.error('Please select at least one product')
return
}
setBulkActionLoading(true)
try {
const response = await fetch('/api/admin/products/bulk', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productIds: selectedProducts,
action
})
})
if (!response.ok) throw new Error('Bulk action failed')
toast.success(`Successfully ${action}d ${selectedProducts.length} products`)
setSelectedProducts([])
fetchProducts()
} catch (error) {
toast.error(`Failed to ${action} products`)
} finally {
setBulkActionLoading(false)
}
}
const handleDeleteProduct = async (productId: string) => {
if (!confirm('Are you sure you want to delete this product?')) return
try {
const response = await fetch(`/api/admin/products/${productId}`, {
method: 'DELETE'
})
if (!response.ok) throw new Error('Delete failed')
toast.success('Product deleted successfully')
fetchProducts()
} catch (error) {
toast.error('Failed to delete product')
}
}
const handleQuickEdit = (product: Product) => {
setEditingProduct(product)
setQuickEditForm({
name: product.name,
description: product.description || '',
price: product.price,
discount: product.discount,
stock: product.stock,
weight: product.weight || ''
})
setQuickEditOpen(true)
}
const handleQuickEditSave = async () => {
if (!editingProduct) return
setQuickEditLoading(true)
try {
const response = await fetch(`/api/admin/products/${editingProduct.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: quickEditForm.name,
description: quickEditForm.description,
price: quickEditForm.price,
discount: quickEditForm.discount,
stock: quickEditForm.stock,
weight: quickEditForm.weight,
// Keep existing values that aren't being edited
images: editingProduct.images,
sku: editingProduct.sku,
isActive: editingProduct.isActive,
categoryId: editingProduct.categoryId || editingProduct.category.id
})
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Update failed')
}
toast.success('Product updated successfully')
setQuickEditOpen(false)
setEditingProduct(null)
fetchProducts()
} catch (error) {
console.error('Quick edit error:', error)
toast.error(error instanceof Error ? error.message : 'Failed to update product')
} finally {
setQuickEditLoading(false)
}
}
const handleQuickEditInputChange = (field: keyof QuickEditFormData, value: string | number) => {
setQuickEditForm(prev => ({
...prev,
[field]: value
}))
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Products</h1>
<p className="text-gray-600">Manage your product catalog</p>
</div>
<div className="flex items-center space-x-2">
<CsvExport
title="Export Products"
description="Export products to a CSV file. Select the columns you want to include."
columns={[
{ key: 'name', label: 'Name' },
{ key: 'description', label: 'Description' },
{ key: 'price', label: 'Price' },
{ key: 'category.name', label: 'Category' },
{ key: 'stock', label: 'Stock' },
{ key: 'active', label: 'Status' },
{ key: 'createdAt', label: 'Created At' }
]}
onExport={async (selectedColumns, filters, onProgress) => {
try {
const response = await fetch('/api/admin/products/export', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
columns: selectedColumns,
filters
}),
})
if (!response.ok) {
throw new Error('Export failed')
}
const result = await response.json()
return {
success: true,
data: result.data || products,
message: 'Export completed successfully'
}
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Export failed',
}
}
}}
>
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
Export
</Button>
</CsvExport>
<CsvImport
title="Import Products"
description="Import products from a CSV file. Download the template to see the required format."
templateColumns={[
{ key: 'name', label: 'Name', required: true },
{ key: 'description', label: 'Description', required: true },
{ key: 'price', label: 'Price', required: true },
{ key: 'categoryId', label: 'Category ID', required: true },
{ key: 'stock', label: 'Stock', required: false },
{ key: 'active', label: 'Active', required: false }
]}
onImport={async (data, onProgress) => {
try {
const response = await fetch('/api/admin/products/import', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ data }),
})
if (!response.ok) {
throw new Error('Import failed')
}
const result = await response.json()
fetchProducts() // Refresh the products list
return result
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Import failed',
}
}
}}
>
<Button variant="outline" size="sm">
<Upload className="h-4 w-4 mr-2" />
Import
</Button>
</CsvImport>
<Button asChild>
<Link href="/admin/products/new">
<Plus className="h-4 w-4 mr-2" />
Add Product
</Link>
</Button>
</div>
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>All Products</CardTitle>
<div className="flex items-center space-x-2">
{selectedProducts.length > 0 && (
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-500">
{selectedProducts.length} selected
</span>
<Button
variant="outline"
size="sm"
onClick={() => handleBulkAction('activate')}
disabled={bulkActionLoading}
>
Activate
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleBulkAction('deactivate')}
disabled={bulkActionLoading}
>
Deactivate
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleBulkAction('delete')}
disabled={bulkActionLoading}
className="text-red-600"
>
Delete
</Button>
</div>
)}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search products..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 w-64"
/>
</div>
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
Export
</Button>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
{loading ? (
<div className="text-center py-8">Loading...</div>
) : (
<div className="border rounded-lg">
<Table>
<TableHeader className="sticky top-0 bg-white z-10">
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={selectedProducts.length === products.length && products.length > 0}
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>Product</TableHead>
<TableHead>SKU</TableHead>
<TableHead>Category</TableHead>
<TableHead>Price</TableHead>
<TableHead>Stock</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
</Table>
<div className="max-h-[55vh] overflow-y-auto">
<Table>
<TableBody>
{products.map((product) => (
<TableRow key={product.id}>
<TableCell className="w-12">
<Checkbox
checked={selectedProducts.includes(product.id)}
onCheckedChange={(checked) => handleSelectProduct(product.id, !!checked)}
/>
</TableCell>
<TableCell>
<div className="flex items-center space-x-3">
<Image
src={product.images[0] || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={product.name}
width={40}
height={40}
className="rounded-lg object-cover"
/>
<div>
<p className="font-medium">{product.name}</p>
{product.discount > 0 && (
<Badge variant="secondary" className="text-xs">
{product.discount}% OFF
</Badge>
)}
</div>
</div>
</TableCell>
<TableCell className="font-mono text-sm">{product.sku}</TableCell>
<TableCell>{product.category.name}</TableCell>
<TableCell>
<div>
{product.discount > 0 ? (
<>
<span className="font-bold text-green-600">
{(product.price - (product.price * product.discount / 100)).toFixed(2)}
</span>
<span className="text-sm text-gray-500 line-through ml-2">
{product.price.toFixed(2)}
</span>
</>
) : (
<span className="font-bold">{product.price.toFixed(2)}</span>
)}
</div>
</TableCell>
<TableCell>
<Badge variant={product.stock > 10 ? 'default' : product.stock > 0 ? 'secondary' : 'destructive'}>
{product.stock}
</Badge>
</TableCell>
<TableCell>
<Badge variant={product.isActive ? 'default' : 'secondary'}>
{product.isActive ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handleQuickEdit(product)}
className="h-8 px-2"
>
<Edit className="h-3 w-3 mr-1" />
Quick Edit
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link href={`/admin/products/${product.id}/edit`}>
<Edit className="h-4 w-4 mr-2" />
Full Edit
</Link>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteProduct(product.id)}
className="text-red-600"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
</CardContent>
</Card>
{/* Quick Edit Dialog */}
<Dialog open={quickEditOpen} onOpenChange={setQuickEditOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Quick Edit Product</DialogTitle>
</DialogHeader>
{editingProduct && (
<div className="space-y-6">
{/* Product Image and Basic Info */}
<div className="flex items-start space-x-4">
<Image
src={editingProduct.images[0] || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={editingProduct.name}
width={80}
height={80}
className="rounded-lg object-cover"
/>
<div className="flex-1">
<p className="text-sm text-gray-500">SKU: {editingProduct.sku}</p>
<p className="text-sm text-gray-500">Category: {editingProduct.category.name}</p>
</div>
</div>
{/* Form Fields */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="quick-name">Product Name</Label>
<Input
id="quick-name"
value={quickEditForm.name}
onChange={(e) => handleQuickEditInputChange('name', e.target.value)}
placeholder="Product name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="quick-weight">Weight</Label>
<Input
id="quick-weight"
value={quickEditForm.weight}
onChange={(e) => handleQuickEditInputChange('weight', e.target.value)}
placeholder="e.g., 1kg, 500g"
/>
</div>
<div className="space-y-2">
<Label htmlFor="quick-price">Price ()</Label>
<Input
id="quick-price"
type="number"
step="0.01"
value={quickEditForm.price}
onChange={(e) => handleQuickEditInputChange('price', parseFloat(e.target.value) || 0)}
placeholder="0.00"
/>
</div>
<div className="space-y-2">
<Label htmlFor="quick-discount">Discount (%)</Label>
<Input
id="quick-discount"
type="number"
min="0"
max="100"
value={quickEditForm.discount}
onChange={(e) => handleQuickEditInputChange('discount', parseFloat(e.target.value) || 0)}
placeholder="0"
/>
</div>
<div className="space-y-2">
<Label htmlFor="quick-stock">Stock Quantity</Label>
<Input
id="quick-stock"
type="number"
min="0"
value={quickEditForm.stock}
onChange={(e) => handleQuickEditInputChange('stock', parseInt(e.target.value) || 0)}
placeholder="0"
/>
</div>
<div className="space-y-2">
<Label htmlFor="quick-discounted-price">Final Price</Label>
<div className="p-2 bg-gray-50 rounded-md text-sm">
{(quickEditForm.price - (quickEditForm.price * quickEditForm.discount / 100)).toFixed(2)}
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="quick-description">Description</Label>
<Textarea
id="quick-description"
value={quickEditForm.description}
onChange={(e) => handleQuickEditInputChange('description', e.target.value)}
placeholder="Product description"
rows={3}
/>
</div>
{/* Action Buttons */}
<div className="flex items-center justify-end space-x-2 pt-4 border-t">
<Button
variant="outline"
onClick={() => setQuickEditOpen(false)}
disabled={quickEditLoading}
>
Cancel
</Button>
<Button
onClick={handleQuickEditSave}
disabled={quickEditLoading}
>
{quickEditLoading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Saving...
</>
) : (
<>
<Save className="h-4 w-4 mr-2" />
Save Changes
</>
)}
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
)
}

304
app/admin/reviews/page.tsx Normal file
View File

@@ -0,0 +1,304 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Star, Search, Check, X, Eye, Trash2 } from 'lucide-react'
import { toast } from 'sonner'
import { formatDistanceToNow } from 'date-fns'
interface Review {
id: string
rating: number
title?: string
comment?: string
isVerified: boolean
isApproved: boolean
helpfulVotes: number
reportCount: number
createdAt: string
user: {
name: string
email: string
}
product: {
name: string
}
}
export default function AdminReviewsPage() {
const [reviews, setReviews] = useState<Review[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState('')
const [statusFilter, setStatusFilter] = useState<'all' | 'pending' | 'approved'>('all')
const fetchReviews = useCallback(async () => {
try {
setLoading(true)
const params = new URLSearchParams({
page: '1',
limit: '50',
admin: 'true'
})
if (search) params.append('search', search)
if (statusFilter !== 'all') {
params.append('approved', statusFilter === 'approved' ? 'true' : 'false')
}
const response = await fetch(`/api/admin/reviews?${params}`)
const data = await response.json()
if (response.ok) {
setReviews(data.reviews || [])
} else {
toast.error('Failed to load reviews')
}
} catch (error) {
toast.error('Failed to load reviews')
} finally {
setLoading(false)
}
}, [search, statusFilter])
useEffect(() => {
fetchReviews()
}, [fetchReviews])
const handleApprove = async (reviewId: string) => {
try {
const response = await fetch(`/api/admin/reviews/${reviewId}/approve`, {
method: 'POST'
})
if (response.ok) {
toast.success('Review approved')
setReviews(prev =>
prev.map(review =>
review.id === reviewId
? { ...review, isApproved: true }
: review
)
)
} else {
toast.error('Failed to approve review')
}
} catch (error) {
toast.error('Failed to approve review')
}
}
const handleReject = async (reviewId: string) => {
try {
const response = await fetch(`/api/admin/reviews/${reviewId}/reject`, {
method: 'POST'
})
if (response.ok) {
toast.success('Review rejected')
setReviews(prev =>
prev.map(review =>
review.id === reviewId
? { ...review, isApproved: false }
: review
)
)
} else {
toast.error('Failed to reject review')
}
} catch (error) {
toast.error('Failed to reject review')
}
}
const handleDelete = async (reviewId: string) => {
if (!confirm('Are you sure you want to delete this review?')) return
try {
const response = await fetch(`/api/admin/reviews/${reviewId}`, {
method: 'DELETE'
})
if (response.ok) {
toast.success('Review deleted')
setReviews(prev => prev.filter(review => review.id !== reviewId))
} else {
toast.error('Failed to delete review')
}
} catch (error) {
toast.error('Failed to delete review')
}
}
const renderStars = (rating: number) => {
return Array.from({ length: 5 }, (_, index) => (
<Star
key={index}
className={`h-4 w-4 ${
index < rating
? 'text-yellow-400 fill-current'
: 'text-gray-300'
}`}
/>
))
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Reviews Management</h1>
<p className="text-gray-600">Moderate customer reviews and feedback</p>
</div>
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>All Reviews</CardTitle>
<div className="flex items-center space-x-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search reviews..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 w-64"
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="rounded-md border border-gray-300 px-3 py-2"
>
<option value="all">All Reviews</option>
<option value="pending">Pending Approval</option>
<option value="approved">Approved</option>
</select>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="text-center py-8">Loading...</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Product</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Rating</TableHead>
<TableHead>Review</TableHead>
<TableHead>Status</TableHead>
<TableHead>Stats</TableHead>
<TableHead>Date</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{reviews.map((review) => (
<TableRow key={review.id}>
<TableCell>
<div>
<p className="font-medium">{review.product.name}</p>
</div>
</TableCell>
<TableCell>
<div>
<p className="font-medium">{review.user.name}</p>
<p className="text-sm text-gray-500">{review.user.email}</p>
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-1">
{renderStars(review.rating)}
<span className="ml-1 text-sm">({review.rating})</span>
</div>
</TableCell>
<TableCell>
<div className="max-w-xs">
{review.title && (
<p className="font-medium text-sm truncate">{review.title}</p>
)}
{review.comment && (
<p className="text-sm text-gray-600 truncate">{review.comment}</p>
)}
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<Badge variant={review.isApproved ? 'default' : 'secondary'}>
{review.isApproved ? 'Approved' : 'Pending'}
</Badge>
{review.isVerified && (
<Badge variant="outline" className="text-green-600 border-green-600">
Verified
</Badge>
)}
</div>
</TableCell>
<TableCell>
<div className="text-sm space-y-1">
<div>👍 {review.helpfulVotes}</div>
{review.reportCount > 0 && (
<div className="text-red-600">🚩 {review.reportCount}</div>
)}
</div>
</TableCell>
<TableCell>
<span className="text-sm text-gray-600">
{formatDistanceToNow(new Date(review.createdAt), { addSuffix: true })}
</span>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
{!review.isApproved && (
<Button
size="sm"
variant="outline"
onClick={() => handleApprove(review.id)}
className="text-green-600 border-green-600"
>
<Check className="h-3 w-3" />
</Button>
)}
{review.isApproved && (
<Button
size="sm"
variant="outline"
onClick={() => handleReject(review.id)}
className="text-orange-600 border-orange-600"
>
<X className="h-3 w-3" />
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={() => handleDelete(review.id)}
className="text-red-600 border-red-600"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
)
}

732
app/admin/settings/page.tsx Normal file
View File

@@ -0,0 +1,732 @@
'use client'
import { useState, useEffect } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { Switch } from '@/components/ui/switch'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import {
Settings,
DollarSign,
Users,
Package,
Mail,
Shield,
Database,
Plus,
Edit,
Trash2,
Save,
RefreshCw
} from 'lucide-react'
import { motion } from 'framer-motion'
import { toast } from 'sonner'
interface CommissionSetting {
id: string
level: number
percentage: number
isActive: boolean
}
interface Category {
id: string
name: string
description: string | null
image: string | null
isActive: boolean
productCount: number
}
interface SystemSettings {
siteName: string
siteDescription: string
supportEmail: string
minimumPayout: number
enableReferrals: boolean
enableCommissions: boolean
maintenanceMode: boolean
allowRegistration: boolean
}
export default function AdminSettingsPage() {
const [commissionSettings, setCommissionSettings] = useState<CommissionSetting[]>([])
const [categories, setCategories] = useState<Category[]>([])
const [systemSettings, setSystemSettings] = useState<SystemSettings>({
siteName: 'Padmaaja Rasooi',
siteDescription: 'Premium quality rice products and grains. Experience the finest rice sourced directly from local farmers.',
supportEmail: 'info@padmajarice.com',
minimumPayout: 1000,
enableReferrals: true,
enableCommissions: true,
maintenanceMode: false,
allowRegistration: true
})
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [editingCommission, setEditingCommission] = useState<CommissionSetting | null>(null)
const [editingCategory, setEditingCategory] = useState<Category | null>(null)
const [newCommission, setNewCommission] = useState({ level: 1, percentage: 10, isActive: true })
const [newCategory, setNewCategory] = useState({ name: '', description: '', image: '' })
useEffect(() => {
fetchSettings()
}, [])
const fetchSettings = async () => {
try {
setLoading(true)
const [commissionsRes, categoriesRes, systemRes] = await Promise.all([
fetch('/api/admin/settings/commissions'),
fetch('/api/admin/settings/categories'),
fetch('/api/admin/settings/system')
])
const [commissionsData, categoriesData, systemData] = await Promise.all([
commissionsRes.json(),
categoriesRes.json(),
systemRes.json()
])
setCommissionSettings(commissionsData.settings || [])
setCategories(categoriesData.categories || [])
setSystemSettings(prev => ({ ...prev, ...systemData }))
} catch (error) {
console.error('Error fetching settings:', error)
toast.error('Failed to load settings')
} finally {
setLoading(false)
}
}
const saveSystemSettings = async () => {
try {
setSaving(true)
const response = await fetch('/api/admin/settings/system', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(systemSettings)
})
if (!response.ok) throw new Error('Failed to save settings')
toast.success('System settings saved successfully')
// Show warning if maintenance mode was enabled
if (systemSettings.maintenanceMode) {
toast.warning('⚠️ Maintenance mode enabled! Public site is now offline.')
} else {
toast.success('✅ Maintenance mode disabled. Public site is now online.')
}
} catch (error) {
console.error('Error saving system settings:', error)
toast.error('Failed to save system settings')
} finally {
setSaving(false)
}
}
const saveCommissionSetting = async (setting: CommissionSetting) => {
try {
const response = await fetch(`/api/admin/settings/commissions/${setting.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(setting)
})
if (!response.ok) throw new Error('Failed to save commission setting')
toast.success('Commission setting saved successfully')
fetchSettings()
setEditingCommission(null)
} catch (error) {
console.error('Error saving commission setting:', error)
toast.error('Failed to save commission setting')
}
}
const addCommissionSetting = async () => {
try {
const response = await fetch('/api/admin/settings/commissions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newCommission)
})
if (!response.ok) throw new Error('Failed to add commission setting')
toast.success('Commission setting added successfully')
setNewCommission({ level: 1, percentage: 10, isActive: true })
fetchSettings()
} catch (error) {
console.error('Error adding commission setting:', error)
toast.error('Failed to add commission setting')
}
}
const deleteCommissionSetting = async (id: string) => {
try {
const response = await fetch(`/api/admin/settings/commissions/${id}`, {
method: 'DELETE'
})
if (!response.ok) throw new Error('Failed to delete commission setting')
toast.success('Commission setting deleted successfully')
fetchSettings()
} catch (error) {
console.error('Error deleting commission setting:', error)
toast.error('Failed to delete commission setting')
}
}
const saveCategory = async (category: Category) => {
try {
const url = category.id.startsWith('new')
? '/api/admin/settings/categories'
: `/api/admin/settings/categories/${category.id}`
const response = await fetch(url, {
method: category.id.startsWith('new') ? 'POST' : 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: category.name,
description: category.description,
image: category.image,
isActive: category.isActive
})
})
if (!response.ok) throw new Error('Failed to save category')
toast.success('Category saved successfully')
fetchSettings()
setEditingCategory(null)
setNewCategory({ name: '', description: '', image: '' })
} catch (error) {
console.error('Error saving category:', error)
toast.error('Failed to save category')
}
}
const deleteCategory = async (id: string) => {
try {
const response = await fetch(`/api/admin/settings/categories/${id}`, {
method: 'DELETE'
})
if (!response.ok) throw new Error('Failed to delete category')
toast.success('Category deleted successfully')
fetchSettings()
} catch (error) {
console.error('Error deleting category:', error)
toast.error('Failed to delete category')
}
}
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
<div className="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100 p-6">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header with maintenance warning */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-4"
>
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-bold bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent">
System Settings
</h1>
<p className="text-gray-600 mt-2">Manage your platform configuration and settings</p>
</div>
<Button onClick={() => fetchSettings()} variant="outline">
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
{systemSettings.maintenanceMode && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center space-x-2">
<Settings className="h-5 w-5 text-red-600" />
<h3 className="font-medium text-red-900">Maintenance Mode Active</h3>
</div>
<div className="mt-2 space-y-1">
<p className="text-sm text-red-700">
The public website is currently offline for regular users.
</p>
<p className="text-sm text-red-600 font-medium">
Admin users (like you) can still access all pages normally.
</p>
</div>
</div>
)}
</motion.div>
<Tabs defaultValue="system" className="space-y-6">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="system" className="flex items-center space-x-2">
<Settings className="h-4 w-4" />
<span>System</span>
</TabsTrigger>
<TabsTrigger value="commissions" className="flex items-center space-x-2">
<DollarSign className="h-4 w-4" />
<span>Commissions</span>
</TabsTrigger>
<TabsTrigger value="categories" className="flex items-center space-x-2">
<Package className="h-4 w-4" />
<span>Categories</span>
</TabsTrigger>
<TabsTrigger value="security" className="flex items-center space-x-2">
<Shield className="h-4 w-4" />
<span>Security</span>
</TabsTrigger>
</TabsList>
{/* System Settings */}
<TabsContent value="system">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-6"
>
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Settings className="h-5 w-5" />
<span>General Settings</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label htmlFor="siteName">Site Name</Label>
<Input
id="siteName"
value={systemSettings.siteName}
onChange={(e) => setSystemSettings(prev => ({ ...prev, siteName: e.target.value }))}
/>
</div>
<div>
<Label htmlFor="supportEmail">Support Email</Label>
<Input
id="supportEmail"
type="email"
value={systemSettings.supportEmail}
onChange={(e) => setSystemSettings(prev => ({ ...prev, supportEmail: e.target.value }))}
/>
</div>
</div>
<div>
<Label htmlFor="siteDescription">Site Description</Label>
<Textarea
id="siteDescription"
value={systemSettings.siteDescription}
onChange={(e) => setSystemSettings(prev => ({ ...prev, siteDescription: e.target.value }))}
rows={3}
/>
</div>
<div>
<Label htmlFor="minimumPayout">Minimum Payout Amount ()</Label>
<Input
id="minimumPayout"
type="number"
value={systemSettings.minimumPayout}
onChange={(e) => setSystemSettings(prev => ({ ...prev, minimumPayout: parseFloat(e.target.value) }))}
/>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>Enable Referral System</Label>
<p className="text-sm text-gray-500">Allow users to refer others</p>
</div>
<Switch
checked={systemSettings.enableReferrals}
onCheckedChange={(checked) => setSystemSettings(prev => ({ ...prev, enableReferrals: checked }))}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>Enable Commissions</Label>
<p className="text-sm text-gray-500">Enable commission calculations</p>
</div>
<Switch
checked={systemSettings.enableCommissions}
onCheckedChange={(checked) => setSystemSettings(prev => ({ ...prev, enableCommissions: checked }))}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>Allow Registration</Label>
<p className="text-sm text-gray-500">Allow new user registrations</p>
</div>
<Switch
checked={systemSettings.allowRegistration}
onCheckedChange={(checked) => setSystemSettings(prev => ({ ...prev, allowRegistration: checked }))}
/>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg bg-red-50 border-red-200">
<div>
<Label className="text-red-900 font-medium">Maintenance Mode</Label>
<p className="text-sm text-red-700 mt-1">Put site in maintenance mode</p>
<p className="text-xs text-red-600 font-medium">
This will take the public site offline for regular users. Admin users will still have full access.
</p>
</div>
<Switch
checked={systemSettings.maintenanceMode}
onCheckedChange={(checked) => setSystemSettings(prev => ({ ...prev, maintenanceMode: checked }))}
/>
</div>
</div>
<Button onClick={saveSystemSettings} disabled={saving}>
<Save className="h-4 w-4 mr-2" />
{saving ? 'Saving...' : 'Save Settings'}
</Button>
</CardContent>
</Card>
</motion.div>
</TabsContent>
{/* Commission Settings */}
<TabsContent value="commissions">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-6"
>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center space-x-2">
<DollarSign className="h-5 w-5" />
<span>Commission Structure</span>
</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
Add Level
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Commission Level</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Level</Label>
<Input
type="number"
value={newCommission.level}
onChange={(e) => setNewCommission(prev => ({ ...prev, level: parseInt(e.target.value) }))}
/>
</div>
<div>
<Label>Percentage (%)</Label>
<Input
type="number"
step="0.1"
value={newCommission.percentage}
onChange={(e) => setNewCommission(prev => ({ ...prev, percentage: parseFloat(e.target.value) }))}
/>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={newCommission.isActive}
onCheckedChange={(checked) => setNewCommission(prev => ({ ...prev, isActive: checked }))}
/>
<Label>Active</Label>
</div>
<Button onClick={addCommissionSetting} className="w-full">
Add Commission Level
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{commissionSettings.map((setting) => (
<div key={setting.id} className="flex items-center justify-between p-4 border rounded-lg">
<div>
<p className="font-medium">Level {setting.level}</p>
<p className="text-sm text-gray-500">{setting.percentage}% commission</p>
</div>
<div className="flex items-center space-x-2">
<Badge variant={setting.isActive ? 'default' : 'secondary'}>
{setting.isActive ? 'Active' : 'Inactive'}
</Badge>
<Button
variant="outline"
size="sm"
onClick={() => setEditingCommission(setting)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => deleteCommissionSetting(setting.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</motion.div>
</TabsContent>
{/* Categories */}
<TabsContent value="categories">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-6"
>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center space-x-2">
<Package className="h-5 w-5" />
<span>Product Categories</span>
</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
Add Category
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Category</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Name</Label>
<Input
value={newCategory.name}
onChange={(e) => setNewCategory(prev => ({ ...prev, name: e.target.value }))}
/>
</div>
<div>
<Label>Description</Label>
<Textarea
value={newCategory.description}
onChange={(e) => setNewCategory(prev => ({ ...prev, description: e.target.value }))}
rows={3}
/>
</div>
<div>
<Label>Image URL</Label>
<Input
value={newCategory.image}
onChange={(e) => setNewCategory(prev => ({ ...prev, image: e.target.value }))}
/>
</div>
<Button
onClick={() => saveCategory({
id: 'new-category',
name: newCategory.name,
description: newCategory.description,
image: newCategory.image,
isActive: true,
productCount: 0
})}
className="w-full"
>
Add Category
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{categories.map((category) => (
<div key={category.id} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h3 className="font-medium">{category.name}</h3>
<Badge variant={category.isActive ? 'default' : 'secondary'}>
{category.isActive ? 'Active' : 'Inactive'}
</Badge>
</div>
<p className="text-sm text-gray-500 mb-2">{category.description}</p>
<p className="text-xs text-gray-400 mb-3">{category.productCount} products</p>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setEditingCategory(category)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => deleteCategory(category.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</motion.div>
</TabsContent>
{/* Security */}
<TabsContent value="security">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-6"
>
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Shield className="h-5 w-5" />
<span>Security Settings</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h3 className="font-medium text-blue-900 mb-2">Database Status</h3>
<p className="text-sm text-blue-700">Database connection is healthy</p>
</div>
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h3 className="font-medium text-yellow-900 mb-2">Environment Variables</h3>
<p className="text-sm text-yellow-700">All required environment variables are configured</p>
</div>
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<h3 className="font-medium text-green-900 mb-2">SSL Certificate</h3>
<p className="text-sm text-green-700">SSL certificate is valid and up to date</p>
</div>
</div>
</CardContent>
</Card>
</motion.div>
</TabsContent>
</Tabs>
{/* Edit Dialogs */}
{editingCommission && (
<Dialog open={!!editingCommission} onOpenChange={() => setEditingCommission(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Commission Setting</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Level</Label>
<Input
type="number"
value={editingCommission.level}
onChange={(e) => setEditingCommission(prev => prev ? { ...prev, level: parseInt(e.target.value) } : null)}
/>
</div>
<div>
<Label>Percentage (%)</Label>
<Input
type="number"
step="0.1"
value={editingCommission.percentage}
onChange={(e) => setEditingCommission(prev => prev ? { ...prev, percentage: parseFloat(e.target.value) } : null)}
/>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={editingCommission.isActive}
onCheckedChange={(checked) => setEditingCommission(prev => prev ? { ...prev, isActive: checked } : null)}
/>
<Label>Active</Label>
</div>
<Button onClick={() => saveCommissionSetting(editingCommission)} className="w-full">
Save Changes
</Button>
</div>
</DialogContent>
</Dialog>
)}
{editingCategory && (
<Dialog open={!!editingCategory} onOpenChange={() => setEditingCategory(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Category</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Name</Label>
<Input
value={editingCategory.name}
onChange={(e) => setEditingCategory(prev => prev ? { ...prev, name: e.target.value } : null)}
/>
</div>
<div>
<Label>Description</Label>
<Textarea
value={editingCategory.description || ''}
onChange={(e) => setEditingCategory(prev => prev ? { ...prev, description: e.target.value } : null)}
rows={3}
/>
</div>
<div>
<Label>Image URL</Label>
<Input
value={editingCategory.image || ''}
onChange={(e) => setEditingCategory(prev => prev ? { ...prev, image: e.target.value } : null)}
/>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={editingCategory.isActive}
onCheckedChange={(checked) => setEditingCategory(prev => prev ? { ...prev, isActive: checked } : null)}
/>
<Label>Active</Label>
</div>
<Button onClick={() => saveCategory(editingCategory)} className="w-full">
Save Changes
</Button>
</div>
</DialogContent>
</Dialog>
)}
</div>
</div>
)
}

190
app/admin/users/page.tsx Normal file
View File

@@ -0,0 +1,190 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Search, Filter, Download, UserPlus } from 'lucide-react'
interface User {
id: string
name: string
email: string
role: 'ADMIN' | 'MEMBER' | 'CUSTOMER' | 'WHOLESALER' | 'PART_TIME'
isActive: boolean
joinedAt: string
referralCode: string
_count: {
referrals: number
orders: number
}
referrer?: {
name: string
email: string
}
}
export default function AdminUsersPage() {
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState('')
const [roleFilter, setRoleFilter] = useState('all')
const [page, setPage] = useState(1)
const fetchUsers = useCallback(async () => {
try {
setLoading(true)
const params = new URLSearchParams({
page: page.toString(),
limit: '10'
})
if (search) params.append('search', search)
if (roleFilter && roleFilter !== 'all') params.append('role', roleFilter)
const response = await fetch(`/api/admin/users?${params}`)
const data = await response.json()
setUsers(data.users || [])
} catch (error) {
console.error('Error fetching users:', error)
} finally {
setLoading(false)
}
}, [search, roleFilter, page])
useEffect(() => {
fetchUsers()
}, [fetchUsers])
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'ADMIN': return 'bg-red-500'
case 'MEMBER': return 'bg-blue-500'
case 'CUSTOMER': return 'bg-gray-500'
default: return 'bg-gray-500'
}
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Users</h1>
<p className="text-gray-600">Manage your platform users</p>
</div>
<Button>
<UserPlus className="h-4 w-4 mr-2" />
Add User
</Button>
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>All Users</CardTitle>
<div className="flex items-center space-x-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search users..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 w-64"
/>
</div>
<Select value={roleFilter} onValueChange={setRoleFilter}>
<SelectTrigger className="w-32">
<SelectValue placeholder="Role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Roles</SelectItem>
<SelectItem value="ADMIN">Admin</SelectItem>
<SelectItem value="MEMBER">Member</SelectItem>
<SelectItem value="CUSTOMER">Customer</SelectItem>
<SelectItem value="WHOLESALER">Wholesaler</SelectItem>
<SelectItem value="PART_TIME">Part Time</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
Export
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="text-center py-8">Loading...</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>User</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead>Referrals</TableHead>
<TableHead>Orders</TableHead>
<TableHead>Joined</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>
<div>
<p className="font-medium">{user.name}</p>
<p className="text-sm text-gray-500">{user.email}</p>
{user.referrer && (
<p className="text-xs text-gray-400">
Referred by: {user.referrer.name}
</p>
)}
</div>
</TableCell>
<TableCell>
<Badge className={`${getRoleBadgeColor(user.role)} text-white`}>
{user.role}
</Badge>
</TableCell>
<TableCell>
<Badge variant={user.isActive ? 'default' : 'secondary'}>
{user.isActive ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell>{user._count.referrals}</TableCell>
<TableCell>{user._count.orders}</TableCell>
<TableCell>
{new Date(user.joinedAt).toLocaleDateString()}
</TableCell>
<TableCell>
<Button variant="ghost" size="sm">
Edit
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,69 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { rankSystem } from '@/lib/ranks'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
// Get user with current rank
const user = await prisma.user.findUnique({
where: { id: userId },
include: {
currentRank: true
}
})
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
// Calculate current metrics
const metrics = await rankSystem.calculateUserMetrics(userId)
// Get all ranks to find next rank
const allRanks = await prisma.rank.findMany({
where: { isActive: true },
orderBy: { order: 'asc' }
})
// Find next rank
const currentRankOrder = user.currentRank?.order || 0
const nextRank = allRanks.find(rank => rank.order > currentRankOrder)
// Get user's achievements
const achievements = await prisma.rankAchievement.findMany({
where: { userId },
include: {
rank: true
},
orderBy: { achievedAt: 'desc' }
})
const response = {
currentRank: user.currentRank,
nextRank: nextRank || null,
metrics,
achievements: achievements.map(achievement => ({
id: achievement.id,
rank: achievement.rank,
achievedAt: achievement.achievedAt.toISOString()
}))
}
return NextResponse.json(response)
} catch (error) {
console.error('Error fetching achievements:', error)
return NextResponse.json(
{ error: 'Failed to fetch achievements' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,195 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
// Check if category exists
const existingCategory = await prisma.category.findUnique({
where: { id },
include: {
_count: {
select: {
products: true
}
}
}
})
if (!existingCategory) {
return NextResponse.json(
{ error: 'Category not found' },
{ status: 404 }
)
}
// Check if category has products
if (existingCategory._count.products > 0) {
return NextResponse.json(
{
error: 'Cannot delete category with associated products',
productCount: existingCategory._count.products
},
{ status: 400 }
)
}
// Delete the category
await prisma.category.delete({
where: { id }
})
return NextResponse.json({
message: 'Category deleted successfully',
deletedCategory: {
id: existingCategory.id,
name: existingCategory.name
}
})
} catch (error: any) {
console.error('Error deleting category:', error)
return NextResponse.json(
{ error: 'Failed to delete category' },
{ status: 500 }
)
}
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
const body = await request.json()
const { name, description, image, isActive } = body
// Check if category exists
const existingCategory = await prisma.category.findUnique({
where: { id }
})
if (!existingCategory) {
return NextResponse.json(
{ error: 'Category not found' },
{ status: 404 }
)
}
// If name is being updated, check for uniqueness
if (name && name !== existingCategory.name) {
const nameExists = await prisma.category.findUnique({
where: { name }
})
if (nameExists) {
return NextResponse.json(
{ error: 'Category name already exists' },
{ status: 400 }
)
}
}
// Update the category
const updatedCategory = await prisma.category.update({
where: { id },
data: {
...(name !== undefined && { name }),
...(description !== undefined && { description }),
...(image !== undefined && { image }),
...(isActive !== undefined && { isActive })
},
include: {
_count: {
select: {
products: true
}
}
}
})
return NextResponse.json(updatedCategory)
} catch (error: any) {
console.error('Error updating category:', error)
// Handle Prisma unique constraint errors
if (error.code === 'P2002') {
return NextResponse.json(
{ error: 'Category name already exists' },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Failed to update category' },
{ status: 500 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
const category = await prisma.category.findUnique({
where: { id },
include: {
_count: {
select: {
products: true
}
},
products: {
select: {
id: true,
name: true,
price: true,
isActive: true
},
take: 10 // Limit to first 10 products for preview
}
}
})
if (!category) {
return NextResponse.json(
{ error: 'Category not found' },
{ status: 404 }
)
}
return NextResponse.json(category)
} catch (error: any) {
console.error('Error fetching category:', error)
return NextResponse.json(
{ error: 'Failed to fetch category' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,73 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET(request: NextRequest) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const search = searchParams.get('search')
const categories = await prisma.category.findMany({
where: search ? {
name: {
contains: search,
mode: 'insensitive'
}
} : {},
include: {
_count: {
select: {
products: true
}
}
},
orderBy: {
createdAt: 'desc'
}
})
return NextResponse.json(categories)
} catch (error) {
console.error('Error fetching categories:', error)
return NextResponse.json(
{ error: 'Failed to fetch categories' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { name, description, image, isActive } = body
const category = await prisma.category.create({
data: {
name,
description,
image,
isActive
}
})
return NextResponse.json(category)
} catch (error) {
console.error('Error creating category:', error)
return NextResponse.json(
{ error: 'Failed to create category' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const settings = await prisma.commissionSettings.findMany({
orderBy: { level: 'asc' },
})
return NextResponse.json(settings)
} catch (error) {
console.error('Commission settings API error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { level, percentage, isActive } = await request.json()
const setting = await prisma.commissionSettings.upsert({
where: { level },
update: { percentage, isActive },
create: { level, percentage, isActive },
})
return NextResponse.json(setting)
} catch (error) {
console.error('Create commission setting error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,37 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id: commissionId } = await params
const { status } = await request.json()
// Validate status
if (!['PENDING', 'APPROVED', 'PAID', 'CANCELLED'].includes(status)) {
return NextResponse.json({ error: 'Invalid status' }, { status: 400 })
}
const commission = await prisma.commission.update({
where: { id: commissionId },
data: { status }
})
return NextResponse.json(commission)
} catch (error) {
console.error('Error updating commission:', error)
return NextResponse.json(
{ error: 'Failed to update commission' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,77 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET(request: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const url = new URL(request.url)
const status = url.searchParams.get('status')
const type = url.searchParams.get('type')
const level = url.searchParams.get('level')
const page = parseInt(url.searchParams.get('page') || '1')
const limit = parseInt(url.searchParams.get('limit') || '50')
const where: any = {}
if (status && status !== 'all') {
where.status = status
}
if (type && type !== 'all') {
where.type = type
}
if (level && level !== 'all') {
where.level = parseInt(level)
}
const commissions = await prisma.commission.findMany({
where,
include: {
user: {
select: {
id: true,
name: true,
email: true
}
},
fromUser: {
select: {
id: true,
name: true,
email: true
}
}
},
orderBy: {
createdAt: 'desc'
},
skip: (page - 1) * limit,
take: limit
})
const total = await prisma.commission.count({ where })
return NextResponse.json({
commissions: commissions || [],
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
})
} catch (error) {
console.error('Error fetching admin commissions:', error)
return NextResponse.json({
commissions: [],
pagination: { page: 1, limit: 50, total: 0, pages: 0 }
})
}
}

View File

@@ -0,0 +1,36 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id: settingId } = await params
const { level, percentage, isActive } = await request.json()
const setting = await prisma.commissionSettings.update({
where: { id: settingId },
data: {
level,
percentage,
isActive
}
})
return NextResponse.json(setting)
} catch (error) {
console.error('Error updating commission setting:', error)
return NextResponse.json(
{ error: 'Failed to update commission setting' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
let settings = await prisma.commissionSettings.findMany({
orderBy: { level: 'asc' }
})
// If no settings exist, create default ones
if (settings.length === 0) {
const defaultSettings = [
{ level: 1, percentage: 10, isActive: true },
{ level: 2, percentage: 5, isActive: true },
{ level: 3, percentage: 3, isActive: true },
{ level: 4, percentage: 2, isActive: true },
{ level: 5, percentage: 1, isActive: true }
]
await prisma.commissionSettings.createMany({
data: defaultSettings
})
settings = await prisma.commissionSettings.findMany({
orderBy: { level: 'asc' }
})
}
return NextResponse.json({ settings: settings || [] })
} catch (error) {
console.error('Error fetching commission settings:', error)
return NextResponse.json({ settings: [] })
}
}

View File

@@ -0,0 +1,66 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Get total commissions count
const totalCommissions = await prisma.commission.count()
// Get amounts by status
const [pendingResult, approvedResult, paidResult] = await Promise.all([
prisma.commission.aggregate({
where: { status: 'PENDING' },
_sum: { amount: true }
}),
prisma.commission.aggregate({
where: { status: 'APPROVED' },
_sum: { amount: true }
}),
prisma.commission.aggregate({
where: { status: 'PAID' },
_sum: { amount: true }
})
])
// Get this month commissions
const startOfMonth = new Date()
startOfMonth.setDate(1)
startOfMonth.setHours(0, 0, 0, 0)
const thisMonthCommissions = await prisma.commission.count({
where: {
createdAt: { gte: startOfMonth }
}
})
// Calculate average commission
const totalAmount = (pendingResult._sum.amount || 0) + (approvedResult._sum.amount || 0) + (paidResult._sum.amount || 0)
const averageCommission = totalCommissions > 0 ? totalAmount / totalCommissions : 0
return NextResponse.json({
totalCommissions: totalCommissions || 0,
pendingAmount: pendingResult._sum.amount || 0,
approvedAmount: approvedResult._sum.amount || 0,
paidAmount: paidResult._sum.amount || 0,
thisMonthCommissions: thisMonthCommissions || 0,
averageCommission: averageCommission || 0
})
} catch (error) {
console.error('Error fetching commission stats:', error)
return NextResponse.json({
totalCommissions: 0,
pendingAmount: 0,
approvedAmount: 0,
paidAmount: 0,
thisMonthCommissions: 0,
averageCommission: 0
})
}
}

View File

@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const orders = await prisma.order.findMany({
take: 10,
orderBy: { createdAt: 'desc' },
include: {
user: {
select: {
name: true,
email: true
}
}
}
})
return NextResponse.json({ orders })
} catch (error) {
console.error('Error fetching recent orders:', error)
return NextResponse.json(
{ error: 'Failed to fetch recent orders' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,128 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Get current date ranges
const now = new Date()
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)
const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0)
// Get current totals
const [totalUsers, totalProducts, totalOrders, totalRevenueResult] = await Promise.all([
prisma.user.count(),
prisma.product.count({ where: { isActive: true } }),
prisma.order.count(),
prisma.order.aggregate({
where: { status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] } },
_sum: { total: true }
})
])
// Get last month data for comparison
const [lastMonthUsers, lastMonthProducts, lastMonthOrders, lastMonthRevenueResult] = await Promise.all([
prisma.user.count({
where: {
joinedAt: {
gte: startOfLastMonth,
lte: endOfLastMonth
}
}
}),
prisma.product.count({
where: {
isActive: true,
createdAt: {
gte: startOfLastMonth,
lte: endOfLastMonth
}
}
}),
prisma.order.count({
where: {
createdAt: {
gte: startOfLastMonth,
lte: endOfLastMonth
}
}
}),
prisma.order.aggregate({
where: {
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] },
createdAt: {
gte: startOfLastMonth,
lte: endOfLastMonth
}
},
_sum: { total: true }
})
])
// Get this month data
const [thisMonthUsers, thisMonthProducts, thisMonthOrders, thisMonthRevenueResult] = await Promise.all([
prisma.user.count({
where: {
joinedAt: { gte: startOfMonth }
}
}),
prisma.product.count({
where: {
isActive: true,
createdAt: { gte: startOfMonth }
}
}),
prisma.order.count({
where: {
createdAt: { gte: startOfMonth }
}
}),
prisma.order.aggregate({
where: {
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] },
createdAt: { gte: startOfMonth }
},
_sum: { total: true }
})
])
// Calculate growth percentages
const calculateGrowth = (current: number, previous: number) => {
if (previous === 0) return current > 0 ? 100 : 0
return ((current - previous) / previous) * 100
}
const totalRevenue = totalRevenueResult._sum.total || 0
const lastMonthRevenue = lastMonthRevenueResult._sum.total || 0
const thisMonthRevenue = thisMonthRevenueResult._sum.total || 0
const userGrowth = calculateGrowth(thisMonthUsers, lastMonthUsers)
const productGrowth = calculateGrowth(thisMonthProducts, lastMonthProducts)
const orderGrowth = calculateGrowth(thisMonthOrders, lastMonthOrders)
const revenueGrowth = calculateGrowth(thisMonthRevenue, lastMonthRevenue)
return NextResponse.json({
totalUsers,
totalProducts,
totalOrders,
totalRevenue,
userGrowth,
productGrowth,
orderGrowth,
revenueGrowth
})
} catch (error) {
console.error('Error fetching dashboard stats:', error)
return NextResponse.json(
{ error: 'Failed to fetch dashboard stats' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Get top products by order count this month
const startOfMonth = new Date()
startOfMonth.setDate(1)
startOfMonth.setHours(0, 0, 0, 0)
const topProducts = await prisma.product.findMany({
take: 5,
include: {
orderItems: {
where: {
order: {
createdAt: { gte: startOfMonth },
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] }
}
},
select: {
quantity: true,
price: true
}
}
}
})
const productsWithStats = topProducts.map(product => {
const orderCount = product.orderItems.reduce((sum, item) => sum + item.quantity, 0)
const totalRevenue = product.orderItems.reduce((sum, item) => sum + (item.price * item.quantity), 0)
return {
id: product.id,
name: product.name,
price: product.price,
orderCount,
totalRevenue
}
}).filter(product => product.orderCount > 0)
.sort((a, b) => b.orderCount - a.orderCount)
.slice(0, 5)
return NextResponse.json({ products: productsWithStats })
} catch (error) {
console.error('Error fetching top products:', error)
return NextResponse.json(
{ error: 'Failed to fetch top products' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,148 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
const order = await prisma.order.findUnique({
where: { id },
include: {
user: {
select: {
id: true,
name: true,
email: true,
phone: true
}
},
orderItems: {
include: {
product: {
select: {
id: true,
name: true,
images: true,
sku: true,
price: true
}
}
}
},
shippingAddress: {
select: {
firstName: true,
lastName: true,
company: true,
address1: true,
address2: true,
city: true,
state: true,
zipCode: true,
country: true,
phone: true
}
}
}
})
if (!order) {
return NextResponse.json({ error: 'Order not found' }, { status: 404 })
}
return NextResponse.json(order)
} catch (error) {
console.error('Error fetching order:', error)
return NextResponse.json(
{ error: 'Failed to fetch order' },
{ status: 500 }
)
}
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
const body = await request.json()
const { status } = body
if (!['PENDING', 'PAID', 'SHIPPED', 'DELIVERED', 'CANCELLED'].includes(status)) {
return NextResponse.json(
{ error: 'Invalid status' },
{ status: 400 }
)
}
const order = await prisma.order.update({
where: { id },
data: {
status,
updatedAt: new Date()
},
include: {
user: {
select: {
id: true,
name: true,
email: true,
phone: true
}
},
orderItems: {
include: {
product: {
select: {
id: true,
name: true,
images: true,
sku: true,
price: true
}
}
}
},
shippingAddress: {
select: {
firstName: true,
lastName: true,
company: true,
address1: true,
address2: true,
city: true,
state: true,
zipCode: true,
country: true,
phone: true
}
}
}
})
return NextResponse.json(order)
} catch (error) {
console.error('Error updating order:', error)
return NextResponse.json(
{ error: 'Failed to update order' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,48 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function PATCH(request: NextRequest) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { orderIds, status } = body
if (!orderIds || !Array.isArray(orderIds) || orderIds.length === 0) {
return NextResponse.json(
{ error: 'Order IDs are required' },
{ status: 400 }
)
}
if (!['PENDING', 'PAID', 'SHIPPED', 'DELIVERED', 'CANCELLED'].includes(status)) {
return NextResponse.json(
{ error: 'Invalid status' },
{ status: 400 }
)
}
await prisma.order.updateMany({
where: {
id: { in: orderIds }
},
data: {
status,
updatedAt: new Date()
}
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error in bulk order update:', error)
return NextResponse.json(
{ error: 'Failed to perform bulk update' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,72 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET(request: NextRequest) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const search = searchParams.get('search')
const status = searchParams.get('status')
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '20')
const skip = (page - 1) * limit
const where: any = {}
if (search) {
where.OR = [
{ id: { contains: search, mode: 'insensitive' } },
{ razorpayOrderId: { contains: search, mode: 'insensitive' } },
{ user: { name: { contains: search, mode: 'insensitive' } } },
{ user: { email: { contains: search, mode: 'insensitive' } } }
]
}
if (status) {
where.status = status
}
const orders = await prisma.order.findMany({
where,
include: {
user: {
select: {
id: true,
name: true,
email: true
}
},
orderItems: {
include: {
product: {
select: {
id: true,
name: true,
images: true
}
}
}
}
},
orderBy: {
createdAt: 'desc'
},
skip,
take: limit
})
return NextResponse.json(orders)
} catch (error) {
console.error('Error fetching orders:', error)
return NextResponse.json(
{ error: 'Failed to fetch orders' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,66 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id: payoutId } = await params
const { status, adminNotes } = await request.json()
// Validate status
if (!['APPROVED', 'REJECTED', 'PAID'].includes(status)) {
return NextResponse.json({ error: 'Invalid status' }, { status: 400 })
}
const result = await prisma.$transaction(async (tx) => {
// Get the payout
const payout = await tx.payout.findUnique({
where: { id: payoutId },
include: { user: true }
})
if (!payout) {
throw new Error('Payout not found')
}
// Update payout
const updatedPayout = await tx.payout.update({
where: { id: payoutId },
data: {
status,
adminNotes,
updatedAt: new Date()
}
})
// If rejecting, restore the amount to user's wallet
if (status === 'REJECTED') {
await tx.wallet.update({
where: { userId: payout.userId },
data: {
balance: { increment: payout.amount }
}
})
}
return updatedPayout
})
return NextResponse.json(result)
} catch (error) {
console.error('Error updating payout:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to update payout' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET(request: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const url = new URL(request.url)
const status = url.searchParams.get('status')
const page = parseInt(url.searchParams.get('page') || '1')
const limit = parseInt(url.searchParams.get('limit') || '50')
const where: any = {}
if (status && status !== 'all') {
where.status = status
}
const payouts = await prisma.payout.findMany({
where,
include: {
user: {
select: {
id: true,
name: true,
email: true
}
}
},
orderBy: {
createdAt: 'desc'
},
skip: (page - 1) * limit,
take: limit
})
const total = await prisma.payout.count({ where })
return NextResponse.json({
payouts,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
})
} catch (error) {
console.error('Error fetching admin payouts:', error)
return NextResponse.json(
{ error: 'Failed to fetch payouts' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,45 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Get total requests
const totalRequests = await prisma.payout.count()
// Get amounts by status
const [pendingResult, approvedResult, paidResult] = await Promise.all([
prisma.payout.aggregate({
where: { status: 'PENDING' },
_sum: { amount: true }
}),
prisma.payout.aggregate({
where: { status: 'APPROVED' },
_sum: { amount: true }
}),
prisma.payout.aggregate({
where: { status: 'PAID' },
_sum: { amount: true }
})
])
return NextResponse.json({
totalRequests,
pendingAmount: pendingResult._sum.amount || 0,
approvedAmount: approvedResult._sum.amount || 0,
paidAmount: paidResult._sum.amount || 0
})
} catch (error) {
console.error('Error fetching payout stats:', error)
return NextResponse.json(
{ error: 'Failed to fetch payout stats' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,114 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
const product = await prisma.product.findUnique({
where: { id },
include: {
category: {
select: {
id: true,
name: true
}
}
}
})
if (!product) {
return NextResponse.json({ error: 'Product not found' }, { status: 404 })
}
return NextResponse.json(product)
} catch (error) {
console.error('Error fetching product:', error)
return NextResponse.json(
{ error: 'Failed to fetch product' },
{ status: 500 }
)
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
const body = await request.json()
const { name, description, price, discount, images, stock, sku, isActive, categoryId, weight } = body
const product = await prisma.product.update({
where: { id },
data: {
name,
description,
price,
discount,
images,
stock,
sku,
isActive,
categoryId,
weight,
slug: name.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-') + '-' + Date.now()
},
include: {
category: true
}
})
return NextResponse.json(product)
} catch (error) {
console.error('Error updating product:', error)
return NextResponse.json(
{ error: 'Failed to update product' },
{ status: 500 }
)
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
await prisma.product.delete({
where: { id }
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error deleting product:', error)
return NextResponse.json(
{ error: 'Failed to delete product' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function PATCH(request: NextRequest) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { productIds, action } = body
if (!productIds || !Array.isArray(productIds) || productIds.length === 0) {
return NextResponse.json(
{ error: 'Product IDs are required' },
{ status: 400 }
)
}
let updateData: any = {}
switch (action) {
case 'activate':
updateData = { isActive: true }
break
case 'deactivate':
updateData = { isActive: false }
break
case 'delete':
await prisma.product.deleteMany({
where: {
id: { in: productIds }
}
})
return NextResponse.json({ success: true })
default:
return NextResponse.json(
{ error: 'Invalid action' },
{ status: 400 }
)
}
await prisma.product.updateMany({
where: {
id: { in: productIds }
},
data: updateData
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error in bulk action:', error)
return NextResponse.json(
{ error: 'Failed to perform bulk action' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,195 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { auth } from '@/auth'
export async function GET(request: NextRequest) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json(
{ success: false, message: 'Unauthorized' },
{ status: 401 }
)
}
const { searchParams } = new URL(request.url)
const selectedColumns = searchParams.get('columns')?.split(',') || []
const categoryId = searchParams.get('categoryId') || ''
const isActive = searchParams.get('isActive') || ''
const limit = parseInt(searchParams.get('limit') || '10000')
// Build where clause based on filters
const whereClause: any = {}
if (categoryId && categoryId !== '') {
whereClause.categoryId = categoryId
}
if (isActive && isActive !== '') {
whereClause.isActive = isActive === 'true'
}
// Fetch products with category information
const products = await prisma.product.findMany({
where: whereClause,
include: {
category: {
select: { name: true }
}
},
take: limit,
orderBy: { createdAt: 'desc' }
})
// Transform data based on selected columns
const transformedData = products.map(product => {
const baseData = {
id: product.id,
name: product.name,
description: product.description || '',
price: product.price,
discount: product.discount,
stock: product.stock,
sku: product.sku,
slug: product.slug,
isActive: product.isActive,
categoryName: product.category?.name || '',
categoryId: product.categoryId || '',
brand: product.brand || '',
origin: product.origin || '',
weight: product.weight || '',
images: product.images.join(', '),
createdAt: product.createdAt.toISOString(),
updatedAt: product.updatedAt.toISOString(),
// Calculated fields
finalPrice: product.price - (product.price * product.discount / 100),
discountAmount: product.price * product.discount / 100,
stockStatus: product.stock > 10 ? 'In Stock' : product.stock > 0 ? 'Low Stock' : 'Out of Stock',
status: product.isActive ? 'Active' : 'Inactive'
}
// Filter by selected columns if specified
if (selectedColumns.length > 0) {
const filteredData: any = {}
selectedColumns.forEach(column => {
if (column in baseData) {
filteredData[column] = (baseData as any)[column]
}
})
return filteredData
}
return baseData
})
return NextResponse.json({
success: true,
data: transformedData,
count: transformedData.length
})
} catch (error) {
console.error('Product export error:', error)
return NextResponse.json(
{ success: false, message: 'Internal server error' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json(
{ success: false, message: 'Unauthorized' },
{ status: 401 }
)
}
const { columns = [], filters = {} } = await request.json()
const selectedColumns = columns || []
const categoryId = filters.categoryId || ''
const isActive = filters.isActive || ''
const limit = parseInt(filters.limit || '10000')
// Build where clause based on filters
const whereClause: any = {}
if (categoryId && categoryId !== '') {
whereClause.categoryId = categoryId
}
if (isActive && isActive !== '') {
whereClause.isActive = isActive === 'true'
}
// Fetch products with category information
const products = await prisma.product.findMany({
where: whereClause,
include: {
category: {
select: { name: true }
}
},
take: limit,
orderBy: { createdAt: 'desc' }
})
// Transform data based on selected columns
const transformedData = products.map(product => {
const baseData = {
id: product.id,
name: product.name,
description: product.description || '',
price: product.price,
discount: product.discount,
stock: product.stock,
sku: product.sku,
slug: product.slug,
isActive: product.isActive,
categoryName: product.category?.name || '',
categoryId: product.categoryId || '',
brand: product.brand || '',
origin: product.origin || '',
weight: product.weight || '',
images: product.images.join(', '),
createdAt: product.createdAt.toISOString(),
updatedAt: product.updatedAt.toISOString(),
// Calculated fields
finalPrice: product.price - (product.price * product.discount / 100),
discountAmount: product.price * product.discount / 100,
stockStatus: product.stock > 10 ? 'In Stock' : product.stock > 0 ? 'Low Stock' : 'Out of Stock',
status: product.isActive ? 'Active' : 'Inactive'
}
// Filter by selected columns if specified
if (selectedColumns.length > 0) {
const filteredData: any = {}
selectedColumns.forEach((column: string) => {
if (column in baseData) {
filteredData[column] = (baseData as any)[column]
}
})
return filteredData
}
return baseData
})
return NextResponse.json({
success: true,
data: transformedData,
count: transformedData.length
})
} catch (error) {
console.error('Product export error:', error)
return NextResponse.json(
{ success: false, message: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,162 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { auth } from '@/auth'
export async function POST(request: NextRequest) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json(
{ success: false, message: 'Unauthorized' },
{ status: 401 }
)
}
const { data } = await request.json()
if (!Array.isArray(data) || data.length === 0) {
return NextResponse.json(
{ success: false, message: 'No products provided' },
{ status: 400 }
)
}
let successCount = 0
let errorCount = 0
const errors: string[] = []
// Process products in batches to avoid overwhelming the database
const batchSize = 50
const batches: any[][] = []
for (let i = 0; i < data.length; i += batchSize) {
batches.push(data.slice(i, i + batchSize))
}
for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
const batch = batches[batchIndex]
try {
const productsToCreate: any[] = []
const productsToUpdate: any[] = []
for (let index = 0; index < batch.length; index++) {
const productData = batch[index]
try {
// Validate required fields
if (!productData.name || !productData.price) {
errors.push(`Row ${batchIndex * batchSize + index + 1}: Missing required fields (name, price)`)
errorCount++
continue
}
// Check if category exists
let categoryId: string | undefined = undefined
if (productData.categoryId) {
const category = await prisma.category.findUnique({
where: { id: productData.categoryId }
})
if (category) {
categoryId = category.id
} else {
errors.push(`Row ${batchIndex * batchSize + index + 1}: Category with ID '${productData.categoryId}' not found`)
errorCount++
continue
}
}
// Generate SKU if not provided
const sku = productData.sku || `PROD-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
// Prepare product data
const productPayload = {
name: productData.name.trim(),
description: productData.description || '',
price: parseFloat(productData.price) || 0,
discount: parseFloat(productData.discount) || 0,
stock: parseInt(productData.stock) || 0,
sku: sku,
slug: productData.slug || productData.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
isActive: productData.active === 'true' || productData.active === true || productData.active === '1' || true,
...(categoryId && { categoryId }),
brand: productData.brand || '',
origin: productData.origin || '',
weight: productData.weight || '',
images: productData.images ? (typeof productData.images === 'string' ? productData.images.split(',').map((img: string) => img.trim()) : []) : []
}
// Check if product exists (by SKU)
const existingProduct = await prisma.product.findUnique({
where: { sku: productPayload.sku }
})
if (existingProduct) {
// Update existing product - create update data without null categoryId
const updatePayload = { ...productPayload }
if (!categoryId) {
delete updatePayload.categoryId
}
productsToUpdate.push({
where: { id: existingProduct.id },
data: updatePayload
})
} else {
// Create new product
productsToCreate.push(productPayload)
}
} catch (error) {
errors.push(`Row ${batchIndex * batchSize + index + 1}: ${error instanceof Error ? error.message : 'Processing error'}`)
errorCount++
}
}
// Create new products
if (productsToCreate.length > 0) {
try {
await prisma.product.createMany({
data: productsToCreate,
skipDuplicates: true
})
successCount += productsToCreate.length
} catch (error) {
errors.push(`Batch ${batchIndex + 1}: Failed to create products - ${error instanceof Error ? error.message : 'Unknown error'}`)
errorCount += productsToCreate.length
}
}
// Update existing products
for (const updateData of productsToUpdate) {
try {
await prisma.product.update(updateData)
successCount++
} catch (error) {
errors.push(`Failed to update product: ${error instanceof Error ? error.message : 'Unknown error'}`)
errorCount++
}
}
} catch (error) {
errors.push(`Batch ${batchIndex + 1}: ${error instanceof Error ? error.message : 'Batch processing error'}`)
errorCount += batch.length
}
}
return NextResponse.json({
success: errorCount === 0,
message: errorCount === 0 ?
`Successfully imported ${successCount} products` :
`Import completed with errors. ${successCount} successful, ${errorCount} failed.`,
successCount,
errorCount,
errors: errors.slice(0, 50) // Limit to first 50 errors
})
} catch (error) {
console.error('Product import error:', error)
return NextResponse.json(
{ success: false, message: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function POST(request: NextRequest) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { name, description, price, discount, images, stock, sku, isActive, categoryId } = body
// Check if SKU already exists
const existingSku = await prisma.product.findUnique({
where: { sku }
})
if (existingSku) {
return NextResponse.json(
{ error: 'SKU already exists' },
{ status: 400 }
)
}
const product = await prisma.product.create({
data: {
name,
description,
price,
discount,
images,
stock,
sku,
isActive,
categoryId,
slug: name.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-') + '-' + Date.now()
},
include: {
category: true
}
})
return NextResponse.json(product)
} catch (error) {
console.error('Error creating product:', error)
return NextResponse.json(
{ error: 'Failed to create product' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
interface Props {
params: Promise<{ id: string }>
}
export async function POST(
request: NextRequest,
{ params }: Props
) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
const review = await prisma.review.update({
where: { id },
data: { isApproved: true }
})
return NextResponse.json({ review })
} catch (error) {
console.error('Error approving review:', error)
return NextResponse.json(
{ error: 'Failed to approve review' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
interface Props {
params: Promise<{ id: string }>
}
export async function POST(
request: NextRequest,
{ params }: Props
) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
const review = await prisma.review.update({
where: { id },
data: { isApproved: false }
})
return NextResponse.json({ review })
} catch (error) {
console.error('Error rejecting review:', error)
return NextResponse.json(
{ error: 'Failed to reject review' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
interface Props {
params: Promise<{ id: string }>
}
export async function DELETE(
request: NextRequest,
{ params }: Props
) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
// Delete related records first
await prisma.reviewHelpfulVote.deleteMany({
where: { reviewId: id }
})
await prisma.reviewReport.deleteMany({
where: { reviewId: id }
})
// Delete the review
await prisma.review.delete({
where: { id }
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error deleting review:', error)
return NextResponse.json(
{ error: 'Failed to delete review' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,91 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET(request: NextRequest) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '20')
const search = searchParams.get('search') || ''
const approved = searchParams.get('approved')
const skip = (page - 1) * limit
const where: any = {}
if (search) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },
{ comment: { contains: search, mode: 'insensitive' } },
{ user: { name: { contains: search, mode: 'insensitive' } } },
{ product: { name: { contains: search, mode: 'insensitive' } } }
]
}
if (approved !== null && approved !== undefined) {
where.isApproved = approved === 'true'
}
const [reviews, total] = await Promise.all([
prisma.review.findMany({
where,
include: {
user: {
select: {
id: true,
name: true,
email: true
}
},
product: {
select: {
id: true,
name: true
}
},
_count: {
select: {
helpfulVotedBy: true,
reportedBy: true
}
}
},
orderBy: {
createdAt: 'desc'
},
skip,
take: limit
}),
prisma.review.count({ where })
])
const reviewsWithCounts = reviews.map(review => ({
...review,
helpfulVotes: review._count.helpfulVotedBy,
reportCount: review._count.reportedBy
}))
return NextResponse.json({
reviews: reviewsWithCounts,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
})
} catch (error) {
console.error('Error fetching admin reviews:', error)
return NextResponse.json(
{ error: 'Failed to fetch reviews' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const categories = await prisma.category.findMany({
include: {
_count: {
select: {
products: true
}
}
},
orderBy: { name: 'asc' }
})
const categoriesWithCount = categories.map(category => ({
id: category.id,
name: category.name,
description: category.description,
image: category.image,
isActive: category.isActive,
productCount: category._count.products
}))
return NextResponse.json({ categories: categoriesWithCount })
} catch (error) {
console.error('Error fetching categories:', error)
return NextResponse.json({ categories: [] })
}
}
export async function POST(request: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { name, description, image, isActive } = await request.json()
const category = await prisma.category.create({
data: { name, description, image, isActive }
})
return NextResponse.json(category)
} catch (error) {
console.error('Error creating category:', error)
return NextResponse.json(
{ error: 'Failed to create category' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const settings = await prisma.commissionSettings.findMany({
orderBy: { level: 'asc' }
})
return NextResponse.json({ settings })
} catch (error) {
console.error('Error fetching commission settings:', error)
return NextResponse.json({ settings: [] })
}
}
export async function POST(request: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { level, percentage, isActive } = await request.json()
const setting = await prisma.commissionSettings.create({
data: { level, percentage, isActive }
})
return NextResponse.json(setting)
} catch (error) {
console.error('Error creating commission setting:', error)
return NextResponse.json(
{ error: 'Failed to create commission setting' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,128 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { clearSettingsCache } from '@/lib/settings'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Get or create system settings
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
}
})
}
return NextResponse.json({
siteName: settings.siteName,
siteDescription: settings.siteDescription,
supportEmail: settings.supportEmail,
minimumPayout: settings.minimumPayout,
enableReferrals: settings.enableReferrals,
enableCommissions: settings.enableCommissions,
maintenanceMode: settings.maintenanceMode,
allowRegistration: settings.allowRegistration
})
} catch (error) {
console.error('Error fetching system settings:', error)
return NextResponse.json(
{ error: 'Failed to fetch system settings' },
{ status: 500 }
)
}
}
export async function PUT(request: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const {
siteName,
siteDescription,
supportEmail,
minimumPayout,
enableReferrals,
enableCommissions,
maintenanceMode,
allowRegistration
} = await request.json()
// Get or create system settings
let settings = await prisma.systemSettings.findFirst()
if (!settings) {
// Create new settings
settings = await prisma.systemSettings.create({
data: {
siteName,
siteDescription,
supportEmail,
minimumPayout,
enableReferrals,
enableCommissions,
maintenanceMode,
allowRegistration
}
})
} else {
// Update existing settings
settings = await prisma.systemSettings.update({
where: { id: settings.id },
data: {
siteName,
siteDescription,
supportEmail,
minimumPayout,
enableReferrals,
enableCommissions,
maintenanceMode,
allowRegistration
}
})
}
// Clear the settings cache so new values are fetched
clearSettingsCache()
return NextResponse.json({
success: true,
settings: {
siteName: settings.siteName,
siteDescription: settings.siteDescription,
supportEmail: settings.supportEmail,
minimumPayout: settings.minimumPayout,
enableReferrals: settings.enableReferrals,
enableCommissions: settings.enableCommissions,
maintenanceMode: settings.maintenanceMode,
allowRegistration: settings.allowRegistration
}
})
} catch (error) {
console.error('Error updating system settings:', error)
return NextResponse.json(
{ error: 'Failed to update system settings' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,76 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET(request: NextRequest) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const search = searchParams.get('search')
const role = searchParams.get('role')
const skip = (page - 1) * limit
const where: any = {}
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } },
]
}
if (role) {
where.role = role
}
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
select: {
id: true,
name: true,
email: true,
role: true,
isActive: true,
joinedAt: true,
referralCode: true,
referrer: {
select: {
name: true,
email: true,
},
},
_count: {
select: {
referrals: true,
orders: true,
},
},
},
skip,
take: limit,
orderBy: { joinedAt: 'desc' },
}),
prisma.user.count({ where }),
])
return NextResponse.json({
users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
})
} catch (error) {
console.error('Admin users API error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,3 @@
import { handlers } from '@/auth'
export const { GET, POST } = handlers

View File

@@ -0,0 +1,72 @@
import { NextRequest, NextResponse } from "next/server";
import { hash } from 'bcryptjs'
import { prisma } from '@/lib/prisma'
import { signUpSchema } from '@/lib/validations/auth'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { name, email, password, phone, referralCode, role } = signUpSchema.parse(body)
// Check if user already exists
const existingUser = await prisma.user.findUnique({
where: { email },
})
if (existingUser) {
return NextResponse.json(
{ error: 'User already exists' },
{ status: 400 }
)
}
// Hash password
const hashedPassword = await hash(password, 12)
// Find referrer if referral code is provided
let referrerId = null
if (referralCode) {
const referrer = await prisma.user.findUnique({
where: { referralCode },
})
if (referrer) {
referrerId = referrer.id
}
}
// Create user
const user = await prisma.user.create({
data: {
name,
email,
password: hashedPassword,
phone,
role: role || 'CUSTOMER',
referrerId,
},
})
// Create wallet for members
if (role === 'MEMBER') {
await prisma.wallet.create({
data: {
userId: user.id,
balance: 0,
totalEarnings: 0,
totalWithdrawn: 0,
},
})
}
return NextResponse.json(
{ message: 'User created successfully', userId: user.id },
{ status: 201 }
)
} catch (error) {
console.error('Signup error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}

105
app/api/categories/route.ts Normal file
View File

@@ -0,0 +1,105 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { categorySchema } from '@/lib/validations/product'
import { DatabaseOptimizer } from '@/lib/database-optimizer'
export async function GET() {
const startTime = Date.now()
try {
// Cache key for categories
const cacheKey = 'categories_navigation_with_counts'
// Try to get from cache first
const cached = await DatabaseOptimizer.getCachedData(cacheKey)
if (cached) {
return NextResponse.json({
...cached,
_performance: {
responseTime: Date.now() - startTime,
cached: true
}
})
}
// Get categories with product counts for better navigation
const categories = await DatabaseOptimizer.executeOptimizedQuery(
'categories_with_product_counts',
async () => {
const cats = await prisma.category.findMany({
where: { isActive: true },
include: {
_count: {
select: {
products: {
where: {
isActive: true,
stock: { gt: 0 }
}
}
}
}
},
orderBy: { name: 'asc' },
})
// Add SEO-friendly data
return cats.map(cat => ({
...cat,
productCount: cat._count.products,
hasProducts: cat._count.products > 0,
_count: undefined // Remove from response
}))
},
1800 // Cache for 30 minutes
)
const responseData = {
categories,
seo: {
totalCategories: categories.length,
categoriesWithProducts: categories.filter(c => c.hasProducts).length
}
}
// Cache the response
await DatabaseOptimizer.setCachedData(cacheKey, responseData, 1800)
return NextResponse.json({
...responseData,
_performance: {
responseTime: Date.now() - startTime,
cached: false
}
})
} catch (error) {
console.error('Categories API error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const session = await auth()
if (!session?.user || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const data = categorySchema.parse(body)
const category = await prisma.category.create({
data,
})
// Invalidate category caches after creation
DatabaseOptimizer.invalidateCache('categories')
DatabaseOptimizer.invalidateCache('category_stats')
return NextResponse.json(category, { status: 201 })
} catch (error) {
console.error('Create category error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,69 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET(request: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const url = new URL(request.url)
const page = parseInt(url.searchParams.get('page') || '1')
const limit = parseInt(url.searchParams.get('limit') || '50')
const status = url.searchParams.get('status')
const type = url.searchParams.get('type')
const level = url.searchParams.get('level')
const where: any = { userId: session.user.id }
if (status && status !== 'all') {
where.status = status
}
if (type && type !== 'all') {
where.type = type
}
if (level && level !== 'all') {
where.level = parseInt(level)
}
const commissions = await prisma.commission.findMany({
where,
include: {
fromUser: {
select: {
name: true,
email: true
}
}
},
orderBy: {
createdAt: 'desc'
},
skip: (page - 1) * limit,
take: limit
})
const total = await prisma.commission.count({ where })
return NextResponse.json({
commissions,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
})
} catch (error) {
console.error('Error fetching commissions:', error)
return NextResponse.json(
{ error: 'Failed to fetch commissions' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,95 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
// Get total earnings
const totalEarningsResult = await prisma.commission.aggregate({
where: {
userId,
status: { in: ['APPROVED', 'PAID'] }
},
_sum: {
amount: true
}
})
// Get pending amount
const pendingAmountResult = await prisma.commission.aggregate({
where: {
userId,
status: 'PENDING'
},
_sum: {
amount: true
}
})
// Get this month earnings
const now = new Date()
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const thisMonthResult = await prisma.commission.aggregate({
where: {
userId,
status: { in: ['APPROVED', 'PAID'] },
createdAt: {
gte: startOfMonth
}
},
_sum: {
amount: true
}
})
// Get total commissions count
const totalCommissions = await prisma.commission.count({
where: { userId }
})
// Get earnings by level
const byLevel = await prisma.commission.groupBy({
by: ['level'],
where: {
userId,
status: { in: ['APPROVED', 'PAID'] }
},
_sum: {
amount: true
},
_count: {
id: true
},
orderBy: {
level: 'asc'
}
})
return NextResponse.json({
totalEarnings: totalEarningsResult._sum.amount || 0,
pendingAmount: pendingAmountResult._sum.amount || 0,
thisMonthEarnings: thisMonthResult._sum.amount || 0,
totalCommissions,
byLevel: byLevel.map(item => ({
level: item.level,
amount: item._sum.amount || 0,
count: item._count.id
}))
})
} catch (error) {
console.error('Error fetching commission stats:', error)
return NextResponse.json(
{ error: 'Failed to fetch commission stats' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,56 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET(request: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const skip = (page - 1) * limit
const [commissions, total] = await Promise.all([
prisma.commission.findMany({
where: { userId: session.user.id },
include: {
fromUser: {
select: {
name: true,
email: true
}
}
},
orderBy: { createdAt: 'desc' },
skip,
take: limit
}),
prisma.commission.count({ where: { userId: session.user.id } })
])
return NextResponse.json({
commissions: commissions.map(commission => ({
id: commission.id,
amount: commission.amount,
level: commission.level,
type: commission.type,
status: commission.status,
createdAt: commission.createdAt,
fromUser: commission.fromUser
})),
total,
pages: Math.ceil(total / limit)
})
} catch (error) {
console.error('Error fetching commissions:', error)
return NextResponse.json(
{ error: 'Failed to fetch commissions' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,171 @@
import { NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
interface TeamMember {
id: string
name: string
email: string
joinedAt: string
totalEarnings: number
directReferrals: number
level: number
children?: TeamMember[]
}
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
// Build genealogy tree
const genealogyTree = await buildGenealogyTree(userId, 0)
// Calculate stats
const teamSize = await calculateTeamSize(userId)
const totalVolume = await calculateTeamVolume(userId)
const levels = await calculateNetworkLevels(userId)
return NextResponse.json({
user: genealogyTree,
teamSize,
totalVolume,
levels
})
} catch (error) {
console.error('Error fetching genealogy data:', error)
return NextResponse.json(
{ error: 'Failed to fetch genealogy data' },
{ status: 500 }
)
}
}
async function buildGenealogyTree(userId: string, level: number): Promise<TeamMember> {
const user = await prisma.user.findUnique({
where: { id: userId },
include: {
wallet: true,
referrals: {
select: {
id: true,
name: true,
email: true,
joinedAt: true
}
}
}
})
if (!user) {
throw new Error('User not found')
}
// Get direct referrals count
const directReferrals = await prisma.user.count({
where: { referrerId: userId }
})
const teamMember: TeamMember = {
id: user.id,
name: user.name || 'Unknown',
email: user.email,
joinedAt: user.joinedAt.toISOString(),
totalEarnings: user.wallet?.totalEarnings || 0,
directReferrals,
level,
children: []
}
// Recursively build children (limit to 5 levels to prevent infinite recursion)
if (level < 5 && user.referrals.length > 0) {
for (const referral of user.referrals) {
const childTree = await buildGenealogyTree(referral.id, level + 1)
teamMember.children!.push(childTree)
}
}
return teamMember
}
async function calculateTeamSize(userId: string): Promise<number> {
const getAllTeamMembers = async (id: string): Promise<string[]> => {
const directReferrals = await prisma.user.findMany({
where: { referrerId: id },
select: { id: true }
})
let allMembers = directReferrals.map(r => r.id)
for (const referral of directReferrals) {
const subTeam = await getAllTeamMembers(referral.id)
allMembers = [...allMembers, ...subTeam]
}
return allMembers
}
const teamMembers = await getAllTeamMembers(userId)
return teamMembers.length
}
async function calculateTeamVolume(userId: string): Promise<number> {
const getAllTeamMembers = async (id: string): Promise<string[]> => {
const directReferrals = await prisma.user.findMany({
where: { referrerId: id },
select: { id: true }
})
let allMembers = directReferrals.map(r => r.id)
for (const referral of directReferrals) {
const subTeam = await getAllTeamMembers(referral.id)
allMembers = [...allMembers, ...subTeam]
}
return allMembers
}
const teamMembers = await getAllTeamMembers(userId)
if (teamMembers.length === 0) return 0
const volume = await prisma.order.aggregate({
where: {
userId: { in: teamMembers },
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] }
},
_sum: { total: true }
})
return volume._sum.total || 0
}
async function calculateNetworkLevels(userId: string): Promise<number> {
const getMaxLevel = async (id: string, currentLevel: number): Promise<number> => {
const directReferrals = await prisma.user.findMany({
where: { referrerId: id },
select: { id: true }
})
if (directReferrals.length === 0) {
return currentLevel
}
let maxLevel = currentLevel + 1
for (const referral of directReferrals) {
const level = await getMaxLevel(referral.id, currentLevel + 1)
maxLevel = Math.max(maxLevel, level)
}
return maxLevel
}
return await getMaxLevel(userId, 0)
}

View File

@@ -0,0 +1,73 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function POST(request: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { amount, bankDetails } = await request.json()
// Check wallet balance
const wallet = await prisma.wallet.findUnique({
where: { userId: session.user.id }
})
if (!wallet || wallet.balance < amount) {
return NextResponse.json({ error: 'Insufficient balance' }, { status: 400 })
}
// Create payout request
const payout = await prisma.payout.create({
data: {
userId: session.user.id,
amount,
bankDetails,
status: 'PENDING'
}
})
// Update wallet balance (deduct the requested amount)
await prisma.wallet.update({
where: { userId: session.user.id },
data: {
balance: { decrement: amount }
}
})
return NextResponse.json(payout)
} catch (error) {
console.error('Error creating payout request:', error)
return NextResponse.json(
{ error: 'Failed to create payout request' },
{ status: 500 }
)
}
}
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const payouts = await prisma.payout.findMany({
where: { userId: session.user.id },
orderBy: { createdAt: 'desc' }
})
return NextResponse.json(payouts)
} catch (error) {
console.error('Error fetching payouts:', error)
return NextResponse.json(
{ error: 'Failed to fetch payouts' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,80 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { CommissionService } from '@/lib/commission'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { id } = await params
// Get user profile
const user = await prisma.user.findUnique({
where: { id },
select: {
id: true,
name: true,
email: true,
phone: true,
address: true,
joinedAt: true,
role: true,
referralCode: true,
isActive: true
}
})
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
// Get team stats
const stats = await CommissionService.getTeamStats(id)
// Get wallet info
const wallet = await prisma.wallet.findUnique({
where: { userId: id }
})
// Get recent commissions
const recentCommissions = await prisma.commission.findMany({
where: { userId: id },
orderBy: { createdAt: 'desc' },
take: 5,
select: {
id: true,
amount: true,
level: true,
type: true,
status: true,
createdAt: true
}
})
const profile = {
...user,
stats: {
...stats,
totalEarnings: wallet?.totalEarnings || 0,
walletBalance: wallet?.balance || 0
},
recentCommissions
}
return NextResponse.json(profile)
} catch (error) {
console.error('Error fetching user profile:', error)
return NextResponse.json(
{ error: 'Failed to fetch profile' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,70 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Get user's direct referrals
const referrals = await prisma.user.findMany({
where: {
referrerId: session.user.id
},
select: {
id: true,
name: true,
email: true,
phone: true,
joinedAt: true,
isActive: true,
orders: {
where: {
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] }
},
select: {
total: true
}
},
// Get commissions earned from this referral
commissionsFrom: {
where: {
userId: session.user.id,
status: { in: ['APPROVED', 'PAID'] }
},
select: {
amount: true
}
}
}
})
const formattedReferrals = referrals.map(referral => ({
id: referral.id,
name: referral.name || 'User',
email: referral.email,
phone: referral.phone,
joinedAt: referral.joinedAt,
isActive: referral.isActive,
totalOrders: referral.orders.length,
totalSpent: referral.orders.reduce((sum, order) => sum + order.total, 0),
commissionEarned: referral.commissionsFrom.reduce((sum, comm) => sum + comm.amount, 0),
status: referral.isActive ? 'ACTIVE' : 'INACTIVE'
}))
return NextResponse.json({
referrals: formattedReferrals
})
} catch (error) {
console.error('Error fetching referrals:', error)
return NextResponse.json(
{ error: 'Failed to fetch referrals' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,89 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
// Get total referrals
const totalReferrals = await prisma.user.count({
where: { referrerId: userId }
})
// Get active referrals
const activeReferrals = await prisma.user.count({
where: {
referrerId: userId,
isActive: true
}
})
// Get pending referrals
const pendingReferrals = await prisma.user.count({
where: {
referrerId: userId,
isActive: false
}
})
// Get total commission earned from referrals
const totalCommissionResult = await prisma.commission.aggregate({
where: {
userId,
status: { in: ['APPROVED', 'PAID'] }
},
_sum: {
amount: true
}
})
// Get this month's stats
const now = new Date()
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const thisMonthReferrals = await prisma.user.count({
where: {
referrerId: userId,
joinedAt: {
gte: startOfMonth
}
}
})
const thisMonthCommissionResult = await prisma.commission.aggregate({
where: {
userId,
status: { in: ['APPROVED', 'PAID'] },
createdAt: {
gte: startOfMonth
}
},
_sum: {
amount: true
}
})
return NextResponse.json({
totalReferrals,
activeReferrals,
pendingReferrals,
totalCommissionEarned: totalCommissionResult._sum.amount || 0,
thisMonthReferrals,
thisMonthCommission: thisMonthCommissionResult._sum.amount || 0
})
} catch (error) {
console.error('Error fetching referral stats:', error)
return NextResponse.json(
{ error: 'Failed to fetch referral stats' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,159 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET(request: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const range = parseInt(searchParams.get('range') || '30')
const type = searchParams.get('type') || 'all'
const userId = session.user.id
const startDate = new Date()
startDate.setDate(startDate.getDate() - range)
// Get commission data
const commissions = await prisma.commission.findMany({
where: {
userId,
createdAt: { gte: startDate }
},
include: {
fromUser: {
select: { name: true, email: true }
}
},
orderBy: { createdAt: 'desc' }
})
const totalCommissions = commissions.reduce((sum, c) => sum + c.amount, 0)
// Calculate commission growth
const previousPeriodStart = new Date(startDate)
previousPeriodStart.setDate(previousPeriodStart.getDate() - range)
const previousCommissions = await prisma.commission.findMany({
where: {
userId,
createdAt: {
gte: previousPeriodStart,
lt: startDate
}
}
})
const previousTotal = previousCommissions.reduce((sum, c) => sum + c.amount, 0)
const growth = previousTotal > 0 ? ((totalCommissions - previousTotal) / previousTotal) * 100 : 0
// Commission by level
const commissionsByLevel = commissions.reduce((acc, c) => {
const existing = acc.find(item => item.level === c.level)
if (existing) {
existing.amount += c.amount
existing.count += 1
} else {
acc.push({ level: c.level, amount: c.amount, count: 1 })
}
return acc
}, [] as { level: number; amount: number; count: number }[])
// Get team data
const directReferrals = await prisma.user.findMany({
where: { referrerId: userId },
select: {
id: true,
joinedAt: true,
orders: {
where: {
createdAt: { gte: startDate },
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] }
}
}
}
})
// Get all team members recursively
const getAllTeamMembers = async (id: string): Promise<string[]> => {
const refs = await prisma.user.findMany({
where: { referrerId: id },
select: { id: true }
})
let allMembers = refs.map(r => r.id)
for (const ref of refs) {
const subTeam = await getAllTeamMembers(ref.id)
allMembers = [...allMembers, ...subTeam]
}
return allMembers
}
const allTeamMemberIds = await getAllTeamMembers(userId)
// Team metrics
const activeMembers = directReferrals.filter(r => r.orders.length > 0).length
const newThisMonth = directReferrals.filter(r =>
new Date(r.joinedAt) >= new Date(new Date().getFullYear(), new Date().getMonth(), 1)
).length
// Sales data
const userOrders = await prisma.order.findMany({
where: {
userId,
createdAt: { gte: startDate },
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] }
}
})
const teamOrders = await prisma.order.findMany({
where: {
userId: { in: allTeamMemberIds },
createdAt: { gte: startDate },
status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] }
}
})
const personalVolume = userOrders.reduce((sum, o) => sum + o.total, 0)
const teamVolume = teamOrders.reduce((sum, o) => sum + o.total, 0)
const reportData = {
commissions: {
total: totalCommissions,
thisMonth: commissions.filter(c =>
new Date(c.createdAt) >= new Date(new Date().getFullYear(), new Date().getMonth(), 1)
).reduce((sum, c) => sum + c.amount, 0),
lastMonth: previousTotal,
growth,
byLevel: commissionsByLevel.sort((a, b) => a.level - b.level),
recent: commissions.slice(0, 10)
},
team: {
totalMembers: allTeamMemberIds.length,
activeMembers,
newThisMonth,
byLevel: [{ level: 1, count: directReferrals.length }] // Simplified for now
},
sales: {
totalVolume: personalVolume + teamVolume,
personalVolume,
teamVolume,
ordersCount: userOrders.length + teamOrders.length
}
}
return NextResponse.json(reportData)
} catch (error) {
console.error('Error fetching reports:', error)
return NextResponse.json(
{ error: 'Failed to fetch reports' },
{ status: 500 }
)
}
}

119
app/api/dashboard/route.ts Normal file
View File

@@ -0,0 +1,119 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
// Get user's wallet
const wallet = await prisma.wallet.findUnique({
where: { userId }
})
// Get commission stats
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 }
})
])
// Get referral stats
const [totalReferrals, activeReferrals] = await Promise.all([
prisma.user.count({
where: { referrerId: userId }
}),
prisma.user.count({
where: {
referrerId: userId,
isActive: true
}
})
])
// Get order stats
const totalOrders = await prisma.order.count({
where: { userId }
})
// Get recent commissions
const recentCommissions = await prisma.commission.findMany({
where: { userId },
include: {
fromUser: {
select: {
name: true,
email: true
}
}
},
orderBy: { createdAt: 'desc' },
take: 5
})
// Get recent orders
const recentOrders = await prisma.order.findMany({
where: { userId },
include: {
orderItems: {
include: {
product: {
select: {
name: true
}
}
}
}
},
orderBy: { createdAt: 'desc' },
take: 5
})
const stats = {
totalEarnings: totalEarnings._sum.amount || 0,
pendingCommissions: pendingCommissions._sum.amount || 0,
thisMonthEarnings: thisMonthEarnings._sum.amount || 0,
currentBalance: wallet?.balance || 0,
totalReferrals,
activeReferrals,
totalOrders,
recentCommissions,
recentOrders
}
return NextResponse.json({ stats })
} catch (error) {
console.error('Error fetching dashboard stats:', error)
return NextResponse.json(
{ error: 'Failed to fetch dashboard stats' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,48 @@
import { NextResponse } from 'next/server'
import { auth } from '@/auth'
import { CommissionService } from '@/lib/commission'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
// Get team stats
const teamStats = await CommissionService.getTeamStats(userId)
// Get wallet info
const wallet = await prisma.wallet.findUnique({
where: { userId }
})
// Get pending commissions
const pendingCommissions = await prisma.commission.aggregate({
where: {
userId,
status: 'PENDING'
},
_sum: { amount: true }
})
const stats = {
...teamStats,
totalEarnings: wallet?.totalEarnings || 0,
walletBalance: wallet?.balance || 0,
pendingCommissions: pendingCommissions._sum.amount || 0
}
return NextResponse.json(stats)
} catch (error) {
console.error('Error fetching dashboard stats:', error)
return NextResponse.json(
{ error: 'Failed to fetch stats' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,100 @@
import { NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
// Get direct referrals
const directReferrals = await prisma.user.findMany({
where: { referrerId: userId },
select: {
id: true,
name: true,
email: true,
role: true,
joinedAt: true,
referrals: {
select: {
id: true
}
}
},
orderBy: { joinedAt: 'desc' }
})
// Get team statistics by levels
const getAllTeamMembers = async (id: string, level: number = 1, maxLevel: number = 5): Promise<string[]> => {
if (level > maxLevel) return []
const refs = await prisma.user.findMany({
where: { referrerId: id },
select: { id: true }
})
let allMembers = refs.map(r => r.id)
if (level < maxLevel) {
for (const ref of refs) {
const subTeam = await getAllTeamMembers(ref.id, level + 1, maxLevel)
allMembers = [...allMembers, ...subTeam]
}
}
return allMembers
}
// Calculate team size per level
const levels = []
for (let level = 1; level <= 5; level++) {
if (level === 1) {
levels.push({ level, count: directReferrals.length })
} else {
// Get members at specific level
const getLevelMembers = async (parentIds: string[], targetLevel: number, currentLevel: number = 1): Promise<string[]> => {
if (currentLevel === targetLevel) return parentIds
const nextLevel = await prisma.user.findMany({
where: { referrerId: { in: parentIds } },
select: { id: true }
})
if (nextLevel.length === 0 || currentLevel >= targetLevel) return []
return getLevelMembers(nextLevel.map(u => u.id), targetLevel, currentLevel + 1)
}
const levelMemberIds = await getLevelMembers([userId], level)
levels.push({ level, count: levelMemberIds.length })
}
}
const allTeamMemberIds = await getAllTeamMembers(userId)
const teamData = {
directReferrals: directReferrals.map(ref => ({
...ref,
referrals: ref.referrals || []
})),
teamStats: {
totalTeamSize: allTeamMemberIds.length,
levels
}
}
return NextResponse.json(teamData)
} catch (error) {
console.error('Error fetching team data:', error)
return NextResponse.json(
{ error: 'Failed to fetch team data' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,71 @@
import { NextResponse } from 'next/server'
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
// Get wallet info
const wallet = await prisma.wallet.findUnique({
where: { userId }
})
// Mock transaction history (you can implement proper transaction tracking)
const commissions = await prisma.commission.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
take: 20,
include: {
fromUser: {
select: { name: true }
}
}
})
const payouts = await prisma.payout.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
take: 20
})
// Combine and format transactions
const transactions = [
...commissions.map(commission => ({
id: commission.id,
type: 'COMMISSION' as const,
amount: commission.amount,
description: `Level ${commission.level} commission from ${commission.fromUser.name}`,
status: commission.status,
createdAt: commission.createdAt
})),
...payouts.map(payout => ({
id: payout.id,
type: 'PAYOUT' as const,
amount: payout.amount,
description: 'Withdrawal request',
status: payout.status,
createdAt: payout.createdAt
}))
].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
return NextResponse.json({
balance: wallet?.balance || 0,
totalEarnings: wallet?.totalEarnings || 0,
totalWithdrawn: wallet?.totalWithdrawn || 0,
transactions
})
} catch (error) {
console.error('Error fetching wallet data:', error)
return NextResponse.json(
{ error: 'Failed to fetch wallet data' },
{ status: 500 }
)
}
}

105
app/api/forms/[id]/route.ts Normal file
View File

@@ -0,0 +1,105 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'
const UpdateFormSchema = z.object({
status: z.enum(['new', 'in_progress', 'resolved', 'closed'])
})
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const formResponse = await prisma.formResponse.findUnique({
where: { id }
})
if (!formResponse) {
return NextResponse.json(
{ success: false, message: 'Form response not found' },
{ status: 404 }
)
}
return NextResponse.json({
success: true,
data: formResponse
})
} catch (error) {
console.error('Error fetching form response:', error)
return NextResponse.json(
{ success: false, message: 'Failed to fetch form response' },
{ status: 500 }
)
}
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
const validatedData = UpdateFormSchema.parse(body)
const formResponse = await prisma.formResponse.update({
where: { id },
data: {
status: validatedData.status,
updatedAt: new Date()
}
})
return NextResponse.json({
success: true,
data: formResponse
})
} catch (error) {
console.error('Error updating form response:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
message: 'Validation error',
errors: error.errors
},
{ status: 400 }
)
}
return NextResponse.json(
{ success: false, message: 'Failed to update form response' },
{ status: 500 }
)
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
await prisma.formResponse.delete({
where: { id }
})
return NextResponse.json({
success: true,
message: 'Form response deleted successfully'
})
} catch (error) {
console.error('Error deleting form response:', error)
return NextResponse.json(
{ success: false, message: 'Failed to delete form response' },
{ status: 500 }
)
}
}

168
app/api/forms/route.ts Normal file
View File

@@ -0,0 +1,168 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'
import { Prisma } from '@prisma/client'
// CORS headers
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
// Validation schema for form submissions
const FormSubmissionSchema = z.object({
formId: z.string().min(1, 'Form ID is required'), // e.g., "contact", "partnership", "bulk_inquiry"
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
phone: z.string().optional(),
subject: z.string().optional(),
message: z.string().min(10, 'Message must be at least 10 characters'),
inquiryType: z.string().optional(),
company: z.string().optional(),
formSource: z.string().optional(), // Optional field to track which form page it came from
// Allow additional fields that will be stored in JSON
}).passthrough()
export async function OPTIONS() {
return new Response(null, {
status: 200,
headers: corsHeaders,
})
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validate the form data
const validatedData = FormSubmissionSchema.parse(body)
// Extract formId from the data
const { formId, ...formData } = validatedData
// Get client information
const forwarded = request.headers.get('x-forwarded-for')
const ipAddress = forwarded ? forwarded.split(',')[0] :
request.headers.get('x-real-ip') ||
'unknown'
const userAgent = request.headers.get('user-agent') || 'unknown'
const referrer = request.headers.get('referer') || null
// Prepare metadata
const metadata = {
ipAddress,
userAgent,
referrer,
timestamp: new Date().toISOString()
}
// Save to database with new simplified schema
const formResponse = await prisma.formResponse.create({
data: {
formId,
data: formData as Prisma.JsonObject, // Store all form data as JSON with proper Prisma type
metadata: metadata as Prisma.JsonObject, // Cast metadata with proper Prisma type
status: 'new'
}
})
// Send email notification (optional)
// You can add email sending logic here using nodemailer or your preferred service
return NextResponse.json(
{
success: true,
message: 'Form submitted successfully',
id: formResponse.id
},
{
status: 200,
headers: corsHeaders
}
)
} catch (error) {
console.error('Form submission error:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
message: 'Validation error',
errors: error.errors
},
{
status: 400,
headers: corsHeaders
}
)
}
return NextResponse.json(
{
success: false,
message: 'Internal server error'
},
{
status: 500,
headers: corsHeaders
}
)
}
}
export async function GET(request: NextRequest) {
try {
// This endpoint could be used for admin to fetch form submissions
// Add authentication/authorization logic here
const { searchParams } = new URL(request.url)
const status = searchParams.get('status')
const formId = searchParams.get('formId') // Changed from formType to formId
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const where: any = {}
if (status) {
where.status = status.toLowerCase() // Changed to lowercase to match new schema
}
if (formId) {
where.formId = formId
}
const [forms, total] = await Promise.all([
prisma.formResponse.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit,
}),
prisma.formResponse.count({ where })
])
return NextResponse.json({
success: true,
data: forms,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
})
} catch (error) {
console.error('Error fetching forms:', error)
return NextResponse.json(
{
success: false,
message: 'Failed to fetch forms'
},
{ status: 500 }
)
}
}

109
app/api/inquiries/route.ts Normal file
View File

@@ -0,0 +1,109 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { emailService } from '@/lib/email'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validate required fields
const requiredFields = [
'companyName', 'contactPerson', 'designation', 'email', 'phone',
'businessType', 'address', 'quantityRequired', 'deliveryLocation', 'message'
]
for (const field of requiredFields) {
if (!body[field]) {
return NextResponse.json(
{ error: `Missing required field: ${field}` },
{ status: 400 }
)
}
}
// Prepare form data for database
const formData = {
formId: 'b2b_inquiry',
data: {
// Company Information
companyName: body.companyName,
contactPerson: body.contactPerson,
designation: body.designation,
email: body.email,
phone: body.phone,
businessType: body.businessType,
gstNumber: body.gstNumber || null,
address: body.address,
// Product Information (if applicable)
productId: body.productId || null,
productName: body.productName || null,
productCategory: body.productCategory || null,
productPrice: body.productPrice || null,
// Requirements
quantityRequired: body.quantityRequired,
quantityUnit: body.quantityUnit || 'tons',
deliveryLocation: body.deliveryLocation,
expectedDeliveryDate: body.expectedDeliveryDate || null,
// Additional Information
message: body.message,
hearAboutUs: body.hearAboutUs || null,
// Metadata
submissionType: 'b2b_inquiry',
submittedAt: body.submittedAt || new Date().toISOString(),
userAgent: request.headers.get('user-agent') || null,
ipAddress: request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip') ||
'unknown'
},
status: 'new',
metadata: {
source: 'website_inquiry_form',
type: 'b2b_bulk_inquiry',
priority: 'high'
}
}
// Save to database
const formResponse = await prisma.formResponse.create({
data: formData
})
// Send email notifications
try {
// Send confirmation email to customer
await emailService.sendB2BInquiryConfirmation({
customerEmail: body.email,
customerName: body.contactPerson,
companyName: body.companyName,
inquiryId: formResponse.id,
productName: body.productName
})
// Send notification to admin
await emailService.sendB2BInquiryAdminNotification({
inquiryData: body,
inquiryId: formResponse.id
})
} catch (emailError) {
console.error('Email notification failed:', emailError)
// Don't fail the API call if email fails
}
return NextResponse.json({
success: true,
message: 'Inquiry submitted successfully',
inquiryId: formResponse.id
})
} catch (error) {
console.error('Error processing B2B inquiry:', error)
return NextResponse.json(
{ error: 'Failed to process inquiry. Please try again.' },
{ status: 500 }
)
}
}

Some files were not shown because too many files have changed in this diff Show More