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

View File

@@ -0,0 +1,14 @@
'use client'
import { LazyFloatingWhatsApp } from './DynamicComponents'
interface ClientFloatingWhatsAppProps {
phoneNumber: string
message: string
position: "bottom-right" | "bottom-left"
showTooltip: boolean
}
export default function ClientFloatingWhatsApp(props: ClientFloatingWhatsAppProps) {
return <LazyFloatingWhatsApp {...props} />
}

View File

@@ -0,0 +1,129 @@
// Dynamic Components for Bundle Optimization
// This file implements lazy loading and code splitting for better performance
'use client'
import dynamic from 'next/dynamic'
import { Suspense } from 'react'
// Loading components
const LoadingSpinner = () => (
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-emerald-600"></div>
</div>
)
const LoadingCard = () => (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 animate-pulse">
<div className="h-48 bg-gray-200 rounded-md mb-4"></div>
<div className="h-4 bg-gray-200 rounded mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
</div>
)
const LoadingForm = () => (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 animate-pulse">
<div className="h-6 bg-gray-200 rounded mb-4"></div>
<div className="space-y-4">
<div className="h-10 bg-gray-200 rounded"></div>
<div className="h-10 bg-gray-200 rounded"></div>
<div className="h-24 bg-gray-200 rounded"></div>
<div className="h-10 bg-gray-200 rounded w-32"></div>
</div>
</div>
)
// Lazy loaded components with proper error boundaries
export const LazyProductCard = dynamic(
() => import('@/components/ProductCard').then(mod => mod.default),
{
loading: () => <LoadingCard />,
ssr: true
}
)
export const LazyContactForm = dynamic(
() => import('@/components/forms/ContactForm').then(mod => mod.default),
{
loading: () => <LoadingForm />,
ssr: false // Contact form doesn't need SSR
}
)
export const LazyFloatingWhatsApp = dynamic(
() => import('@/components/FloatingWhatsApp').then(mod => mod.default),
{
loading: () => null, // No loading state for floating elements
ssr: false // WhatsApp widget doesn't need SSR
}
)
export const LazyImageSlider = dynamic(
() => import('@/components/ImageSlider').then(mod => mod.default),
{
loading: () => <LoadingSpinner />,
ssr: true
}
)
// Preload critical components for better UX
export const preloadCriticalComponents = () => {
if (typeof window !== 'undefined') {
// Preload on interaction or after 3 seconds
const preloadTimer = setTimeout(() => {
import('@/components/ProductCard')
import('@/components/FloatingWhatsApp')
}, 3000)
// Preload on user interaction
const preloadOnInteraction = () => {
import('@/components/forms/ContactForm')
clearTimeout(preloadTimer)
document.removeEventListener('mouseover', preloadOnInteraction)
document.removeEventListener('touchstart', preloadOnInteraction)
}
document.addEventListener('mouseover', preloadOnInteraction, { once: true })
document.addEventListener('touchstart', preloadOnInteraction, { once: true })
}
}
// Wrapper component with Suspense for better error handling
export const DynamicComponentWrapper = ({
children,
fallback = <LoadingSpinner />
}: {
children: React.ReactNode
fallback?: React.ReactNode
}) => (
<Suspense fallback={fallback}>
{children}
</Suspense>
)
// Bundle optimization utility
export const optimizeBundle = {
// Check if component should be loaded
shouldLoad: (component: string) => {
if (typeof window === 'undefined') return true
// Load based on viewport, connection, and device capabilities
const isInViewport = 'IntersectionObserver' in window
const hasGoodConnection = (navigator as any).connection?.effectiveType?.includes('4g') ?? true
const deviceMemory = (navigator as any).deviceMemory
const hasEnoughMemory = deviceMemory ? deviceMemory > 4 : true
return isInViewport && hasGoodConnection && hasEnoughMemory
},
// Prefetch resources
prefetch: (resources: string[]) => {
if (typeof window !== 'undefined') {
resources.forEach(resource => {
const link = document.createElement('link')
link.rel = 'prefetch'
link.href = resource
document.head.appendChild(link)
})
}
}
}

View File

@@ -0,0 +1,141 @@
'use client'
import { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { MessageCircle, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
interface FloatingWhatsAppProps {
phoneNumber: string
message?: string
position?: 'bottom-right' | 'bottom-left'
showTooltip?: boolean
}
export default function FloatingWhatsApp({
phoneNumber,
message = "Hello! I'm interested in your products.",
position = 'bottom-right',
showTooltip = true
}: FloatingWhatsAppProps) {
const [isVisible, setIsVisible] = useState(false)
const [showTooltipState, setShowTooltipState] = useState(false)
useEffect(() => {
// Show the button after 3 seconds
const timer = setTimeout(() => {
setIsVisible(true)
// Show tooltip after button appears (if enabled)
if (showTooltip) {
setTimeout(() => {
setShowTooltipState(true)
}, 1000)
// Hide tooltip after 5 seconds
setTimeout(() => {
setShowTooltipState(false)
}, 6000)
}
}, 3000)
return () => clearTimeout(timer)
}, [showTooltip])
const handleWhatsAppClick = () => {
const encodedMessage = encodeURIComponent(message)
const whatsappUrl = `https://wa.me/${phoneNumber}?text=${encodedMessage}`
window.open(whatsappUrl, '_blank')
}
const positionClasses = {
'bottom-right': 'bottom-6 right-6',
'bottom-left': 'bottom-6 left-6'
}
return (
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
transition={{
type: "spring",
stiffness: 260,
damping: 20
}}
className={`fixed ${positionClasses[position]} z-50 md:mb-0 mb-12`}
>
<div className="relative">
{/* Tooltip */}
<AnimatePresence>
{showTooltipState && (
<motion.div
initial={{ opacity: 0, x: position === 'bottom-right' ? 20 : -20, y: 20 }}
animate={{ opacity: 1, x: 0, y: 0 }}
exit={{ opacity: 0, x: position === 'bottom-right' ? 20 : -20, y: 20 }}
transition={{ duration: 0.3 }}
className={`absolute ${
position === 'bottom-right'
? 'right-16 bottom-0'
: 'left-16 bottom-0'
} mb-2`}
>
<div className="bg-white rounded-lg shadow-lg p-3 max-w-xs relative border">
<button
onClick={() => setShowTooltipState(false)}
className="absolute -top-2 -right-2 bg-gray-100 hover:bg-gray-200 rounded-full p-1 transition-colors"
>
<X className="h-3 w-3 text-gray-600" />
</button>
<p className="text-sm text-gray-700 mb-2">
👋 Need help? Chat with us on WhatsApp!
</p>
<p className="text-xs text-gray-500">
We typically reply within minutes
</p>
{/* Arrow */}
<div className={`absolute top-1/2 transform -translate-y-1/2 ${
position === 'bottom-right'
? '-right-2 border-l-white border-l-8 border-t-transparent border-b-transparent border-t-8 border-b-8'
: '-left-2 border-r-white border-r-8 border-t-transparent border-b-transparent border-t-8 border-b-8'
} w-0 h-0`}></div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* WhatsApp Button */}
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
className="relative"
>
<Button
onClick={handleWhatsAppClick}
className="h-14 w-14 rounded-full bg-green-500 hover:bg-green-600 text-white shadow-lg hover:shadow-xl transition-all duration-300 p-0 relative overflow-hidden group"
aria-label="Contact us on WhatsApp"
>
{/* Ripple effect background */}
<div className="absolute inset-0 bg-green-400 rounded-full opacity-75 animate-ping"></div>
{/* WhatsApp icon */}
<div className="relative z-10">
<svg
className="h-7 w-7"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893A11.821 11.821 0 0020.893 3.488"/>
</svg>
</div>
</Button>
</motion.div>
</div>
</motion.div>
)}
</AnimatePresence>
)
}

191
components/ImageSlider.tsx Normal file
View File

@@ -0,0 +1,191 @@
'use client'
import { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import Image from 'next/image'
import { ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'
interface ImageSliderProps {
images: {
src: string
alt: string
title?: string
}[]
autoPlayInterval?: number
}
export default function ImageSlider({ images, autoPlayInterval = 5000 }: ImageSliderProps) {
const [currentIndex, setCurrentIndex] = useState(0)
const [imagesLoaded, setImagesLoaded] = useState<boolean[]>(new Array(images.length).fill(false))
const [isLoading, setIsLoading] = useState(true)
// Preload all images
useEffect(() => {
const preloadImages = async () => {
const imagePromises = images.map((image, index) => {
return new Promise<void>((resolve, reject) => {
const img = new window.Image()
img.onload = () => {
setImagesLoaded(prev => {
const newLoaded = [...prev]
newLoaded[index] = true
return newLoaded
})
resolve()
}
img.onerror = () => {
console.warn(`Failed to load image: ${image.src}`)
setImagesLoaded(prev => {
const newLoaded = [...prev]
newLoaded[index] = true // Mark as loaded even if failed to avoid infinite loading
return newLoaded
})
resolve() // Don't reject to avoid breaking the Promise.all
}
img.src = image.src
})
})
try {
await Promise.all(imagePromises)
setIsLoading(false)
} catch (error) {
console.error('Error preloading images:', error)
setIsLoading(false)
}
}
preloadImages()
}, [images])
useEffect(() => {
if (autoPlayInterval > 0 && !isLoading) {
const interval = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % images.length)
}, autoPlayInterval)
return () => clearInterval(interval)
}
}, [images.length, autoPlayInterval, isLoading])
const goToPrevious = () => {
setCurrentIndex((prev) => (prev - 1 + images.length) % images.length)
}
const goToNext = () => {
setCurrentIndex((prev) => (prev + 1) % images.length)
}
const goToSlide = (index: number) => {
setCurrentIndex(index)
}
return (
<div className="relative w-full overflow-hidden rounded-lg" style={{ aspectRatio: '21/9' }}>
{/* Loading State */}
{isLoading && (
<div className="absolute inset-0 bg-gray-200 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-8 h-8 animate-spin text-orange-500 mx-auto mb-2" />
<p className="text-gray-600 text-sm">Loading images...</p>
</div>
</div>
)}
{/* Image Container */}
<AnimatePresence mode="wait">
<motion.div
key={currentIndex}
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100 }}
transition={{ duration: 0.5 }}
className="relative w-full h-full"
>
<Image
src={images[currentIndex].src}
alt={images[currentIndex].alt}
fill
className="object-cover"
priority={currentIndex === 0}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R//2Q=="
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 70vw"
onLoad={() => {
setImagesLoaded(prev => {
const newLoaded = [...prev]
newLoaded[currentIndex] = true
return newLoaded
})
}}
/>
{/* Image Loading Skeleton */}
{!imagesLoaded[currentIndex] && (
<div className="absolute inset-0 bg-gray-200 animate-pulse flex items-center justify-center">
<div className="text-gray-400">
<Loader2 className="w-6 h-6 animate-spin" />
</div>
</div>
)}
{/* Gradient Overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent" />
{/* Title Overlay */}
{images[currentIndex].title && (
<div className="absolute bottom-8 left-8 right-8">
<motion.h3
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="text-2xl md:text-3xl font-bold text-white drop-shadow-lg"
>
{images[currentIndex].title}
</motion.h3>
</div>
)}
</motion.div>
</AnimatePresence>
{/* Navigation Arrows - Only show when not loading */}
{!isLoading && (
<>
<button
onClick={goToPrevious}
className="absolute left-4 top-1/2 -translate-y-1/2 bg-white/20 backdrop-blur-sm hover:bg-white/30 text-white p-2 rounded-full transition-all duration-200"
aria-label="Previous image"
>
<ChevronLeft className="w-6 h-6" />
</button>
<button
onClick={goToNext}
className="absolute right-4 top-1/2 -translate-y-1/2 bg-white/20 backdrop-blur-sm hover:bg-white/30 text-white p-2 rounded-full transition-all duration-200"
aria-label="Next image"
>
<ChevronRight className="w-6 h-6" />
</button>
</>
)}
{/* Dots Indicator - Only show when not loading */}
{!isLoading && (
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex space-x-2">
{images.map((_, index) => (
<button
key={index}
onClick={() => goToSlide(index)}
className={`w-3 h-3 rounded-full transition-all duration-200 ${
index === currentIndex
? 'bg-white scale-110'
: 'bg-white/50 hover:bg-white/70'
}`}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,343 @@
'use client'
import { lazy, Suspense } from 'react'
import { ProductGridSkeleton } from '@/components/ui/LazyLoader'
// Lazy load heavy components - Client side only
const ProductGrid = lazy(() => import('@/components/shop/ProductGrid'))
const OurValues = lazy(() => import('@/components/sections/OurValues'))
const AboutSection = lazy(() => import('@/components/sections/AboutSection'))
const HeroSection = lazy(() => import('@/components/sections/HeroSection'))
const CertificationsSection = lazy(() => import('@/components/sections/CertificationsSection'))
const KashminaSection = lazy(() => import('@/components/sections/KashminaSection'))
const ManufacturingSection = lazy(() => import('@/components/sections/ManufacturingSection'))
const StatsSection = lazy(() => import('@/components/sections/StatsSection'))
const SustainabilitySection = lazy(() => import('@/components/sections/SustainabilitySection'))
const NewsSection = lazy(() => import('@/components/sections/NewsSection'))
// Lazy Product Grid with skeleton
export function LazyProductGrid(props: any) {
return (
<Suspense fallback={<ProductGridSkeleton />}>
<ProductGrid {...props} />
</Suspense>
)
}
// Lazy Our Values with skeleton
export function LazyOurValues() {
const fallback = (
<div className="py-20 bg-gradient-to-br from-slate-50 to-blue-50">
<div className="max-w-7xl mx-auto px-3 sm:px-6 lg:px-12">
<div className="text-center space-y-4 mb-16">
<div className="h-6 w-24 bg-gray-200 rounded mx-auto animate-pulse"></div>
<div className="h-8 w-64 bg-gray-200 rounded mx-auto animate-pulse"></div>
<div className="h-4 w-96 bg-gray-200 rounded mx-auto animate-pulse"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-64 bg-white rounded-lg shadow-lg animate-pulse">
<div className="p-8 space-y-4">
<div className="w-16 h-16 bg-gray-200 rounded-full mx-auto"></div>
<div className="h-6 w-32 bg-gray-200 rounded mx-auto"></div>
<div className="h-4 w-full bg-gray-200 rounded"></div>
<div className="h-4 w-3/4 bg-gray-200 rounded mx-auto"></div>
</div>
</div>
))}
</div>
</div>
</div>
)
return (
<Suspense fallback={fallback}>
<OurValues />
</Suspense>
)
}
// Lazy About Section with skeleton
export function LazyAboutSection() {
const fallback = (
<div className="py-20 bg-white">
<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 items-center">
<div className="space-y-6">
<div className="h-8 w-64 bg-gray-200 rounded animate-pulse"></div>
<div className="space-y-3">
<div className="h-4 w-full bg-gray-200 rounded animate-pulse"></div>
<div className="h-4 w-full bg-gray-200 rounded animate-pulse"></div>
<div className="h-4 w-3/4 bg-gray-200 rounded animate-pulse"></div>
</div>
<div className="h-10 w-32 bg-gray-200 rounded animate-pulse"></div>
</div>
<div className="h-96 bg-gray-200 rounded-lg animate-pulse"></div>
</div>
</div>
</div>
)
return (
<Suspense fallback={fallback}>
<AboutSection />
</Suspense>
)
}
// Lazy Hero Section with skeleton
export function LazyHeroSection() {
const fallback = (
<div className="relative h-screen bg-gradient-to-br from-emerald-50 to-blue-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full flex items-center">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center w-full">
<div className="space-y-8">
<div className="h-12 w-96 bg-gray-200 rounded animate-pulse"></div>
<div className="space-y-3">
<div className="h-4 w-full bg-gray-200 rounded animate-pulse"></div>
<div className="h-4 w-3/4 bg-gray-200 rounded animate-pulse"></div>
</div>
<div className="flex space-x-4">
<div className="h-12 w-32 bg-gray-200 rounded animate-pulse"></div>
<div className="h-12 w-32 bg-gray-200 rounded animate-pulse"></div>
</div>
</div>
<div className="h-96 bg-gray-200 rounded-lg animate-pulse"></div>
</div>
</div>
</div>
)
return (
<Suspense fallback={fallback}>
<HeroSection />
</Suspense>
)
}
// Lazy Certifications Section with skeleton
export function LazyCertificationsSection() {
const fallback = (
<div className="py-12 md:py-16 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center space-y-4 mb-8 md:mb-12">
<div className="h-8 w-48 bg-gray-200 rounded mx-auto animate-pulse"></div>
<div className="h-4 w-96 bg-gray-200 rounded mx-auto animate-pulse"></div>
</div>
<div className="flex flex-wrap justify-center items-center gap-6 md:gap-8 lg:gap-12">
{Array.from({ length: 7 }).map((_, i) => (
<div key={i} className="text-center animate-pulse">
<div className="w-20 h-20 md:w-24 md:h-24 lg:w-28 lg:h-28 bg-gray-200 rounded-full mx-auto"></div>
<div className="h-3 w-12 bg-gray-200 rounded mt-2 mx-auto"></div>
</div>
))}
</div>
<div className="text-center mt-8 md:mt-12">
<div className="h-4 w-96 bg-gray-200 rounded mx-auto animate-pulse"></div>
</div>
</div>
</div>
)
return (
<Suspense fallback={fallback}>
<CertificationsSection />
</Suspense>
)
}
// Lazy Kashmina Section with skeleton
export function LazyKashminaSection() {
const fallback = (
<div className="py-16 md:py-24 bg-gradient-to-br from-amber-50 to-red-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center space-y-4 mb-12 md:mb-16">
<div className="h-6 w-32 bg-gray-200 rounded-full mx-auto animate-pulse"></div>
<div className="h-12 w-48 bg-gray-200 rounded mx-auto animate-pulse"></div>
<div className="h-5 w-96 bg-gray-200 rounded mx-auto animate-pulse"></div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center mb-16">
<div className="space-y-6">
<div className="h-8 w-64 bg-gray-200 rounded animate-pulse"></div>
<div className="space-y-3">
<div className="h-4 w-full bg-gray-200 rounded animate-pulse"></div>
<div className="h-4 w-full bg-gray-200 rounded animate-pulse"></div>
<div className="h-4 w-3/4 bg-gray-200 rounded animate-pulse"></div>
</div>
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center gap-3">
<div className="w-5 h-5 bg-gray-200 rounded-full animate-pulse"></div>
<div className="h-4 w-48 bg-gray-200 rounded animate-pulse"></div>
</div>
))}
</div>
<div className="flex gap-4">
<div className="h-12 w-40 bg-gray-200 rounded-xl animate-pulse"></div>
<div className="h-12 w-32 bg-gray-200 rounded-xl animate-pulse"></div>
</div>
</div>
<div className="h-96 bg-gray-200 rounded-3xl animate-pulse"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="bg-white rounded-lg shadow-lg p-6 animate-pulse">
<div className="w-12 h-12 bg-gray-200 rounded-xl mb-4"></div>
<div className="h-5 w-32 bg-gray-200 rounded mb-2"></div>
<div className="h-4 w-full bg-gray-200 rounded"></div>
</div>
))}
</div>
</div>
</div>
)
return (
<Suspense fallback={fallback}>
<KashminaSection />
</Suspense>
)
}
// Lazy Manufacturing Section with skeleton
export function LazyManufacturingSection() {
const fallback = (
<div className="py-16 lg:py-24 bg-gradient-to-br from-slate-50 via-white to-emerald-50/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center space-y-4 mb-16">
<div className="h-6 w-32 bg-gray-200 rounded-full mx-auto animate-pulse"></div>
<div className="h-12 w-96 bg-gray-200 rounded mx-auto animate-pulse"></div>
<div className="h-5 w-[600px] bg-gray-200 rounded mx-auto animate-pulse"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-20">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="bg-white rounded-2xl p-6 shadow-lg animate-pulse">
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-gray-200 rounded-xl"></div>
<div className="text-right">
<div className="h-6 w-16 bg-gray-200 rounded"></div>
<div className="h-3 w-20 bg-gray-200 rounded mt-1"></div>
</div>
</div>
<div className="h-5 w-32 bg-gray-200 rounded mb-2"></div>
<div className="h-4 w-full bg-gray-200 rounded"></div>
</div>
))}
</div>
</div>
</div>
)
return (
<Suspense fallback={fallback}>
<ManufacturingSection />
</Suspense>
)
}
// Lazy Stats Section with skeleton
export function LazyStatsSection() {
const fallback = (
<div className="py-16 lg:py-24 bg-gradient-to-br from-gray-900 via-slate-800 to-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center space-y-4 mb-16">
<div className="h-6 w-24 bg-gray-700 rounded-full mx-auto animate-pulse"></div>
<div className="h-12 w-96 bg-gray-700 rounded mx-auto animate-pulse"></div>
<div className="h-5 w-[600px] bg-gray-700 rounded mx-auto animate-pulse"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="bg-white/5 rounded-2xl p-8 border border-white/10 animate-pulse">
<div className="w-16 h-16 bg-gray-700 rounded-2xl mb-6"></div>
<div className="h-12 w-20 bg-gray-700 rounded mb-2"></div>
<div className="h-5 w-32 bg-gray-700 rounded mb-3"></div>
<div className="h-4 w-full bg-gray-700 rounded"></div>
</div>
))}
</div>
</div>
</div>
)
return (
<Suspense fallback={fallback}>
<StatsSection />
</Suspense>
)
}
// Lazy Sustainability Section with skeleton
export function LazySustainabilitySection() {
const fallback = (
<div className="py-16 lg:py-24 bg-gradient-to-br from-green-50 via-emerald-50 to-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center space-y-4 mb-16">
<div className="h-6 w-32 bg-gray-200 rounded-full mx-auto animate-pulse"></div>
<div className="h-12 w-96 bg-gray-200 rounded mx-auto animate-pulse"></div>
<div className="h-5 w-[600px] bg-gray-200 rounded mx-auto animate-pulse"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-20">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="bg-white rounded-2xl shadow-lg p-6 border border-gray-100 animate-pulse">
<div className="w-16 h-16 bg-gray-200 rounded-2xl mb-6"></div>
<div className="h-5 w-32 bg-gray-200 rounded mb-3"></div>
<div className="h-4 w-full bg-gray-200 rounded mb-4"></div>
<div className="h-6 w-24 bg-gray-200 rounded-full"></div>
</div>
))}
</div>
</div>
</div>
)
return (
<Suspense fallback={fallback}>
<SustainabilitySection />
</Suspense>
)
}
// Lazy News Section with skeleton
export function LazyNewsSection() {
const fallback = (
<div className="py-16 lg:py-24 bg-gradient-to-br from-slate-50 via-white to-blue-50/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center space-y-4 mb-16">
<div className="h-6 w-32 bg-gray-200 rounded-full mx-auto animate-pulse"></div>
<div className="h-12 w-64 bg-gray-200 rounded mx-auto animate-pulse"></div>
<div className="h-5 w-[500px] bg-gray-200 rounded mx-auto animate-pulse"></div>
</div>
<div className="bg-white rounded-3xl shadow-2xl overflow-hidden border border-gray-100 mb-16 animate-pulse">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="h-64 lg:h-80 bg-gray-200"></div>
<div className="p-8 lg:p-12 space-y-4">
<div className="h-4 w-32 bg-gray-200 rounded"></div>
<div className="h-8 w-full bg-gray-200 rounded"></div>
<div className="h-4 w-full bg-gray-200 rounded"></div>
<div className="h-4 w-3/4 bg-gray-200 rounded"></div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="bg-white rounded-2xl shadow-lg overflow-hidden border border-gray-100 animate-pulse">
<div className="h-48 bg-gray-200"></div>
<div className="p-6 space-y-3">
<div className="h-4 w-32 bg-gray-200 rounded"></div>
<div className="h-5 w-full bg-gray-200 rounded"></div>
<div className="h-4 w-full bg-gray-200 rounded"></div>
</div>
</div>
))}
</div>
</div>
</div>
)
return (
<Suspense fallback={fallback}>
<NewsSection />
</Suspense>
)
}

View File

@@ -0,0 +1,295 @@
'use client'
import React, { lazy, Suspense, useEffect, useState, useRef } from 'react'
import { ProductGridSkeleton } from '@/components/ui/LazyLoader'
// Helper to detect mobile devices
const isMobile = () => {
if (typeof window === 'undefined') return false
return window.innerWidth <= 768 || /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
}
// More aggressive lazy loading for mobile devices
const HeroSection = lazy(() => import('@/components/sections/HeroSection'))
const AboutSection = lazy(() => import('@/components/sections/AboutSection'))
const KashminaSection = lazy(() =>
isMobile()
? import('@/components/sections/KashminaSection').then(module => ({ default: module.default }))
: import('@/components/sections/KashminaSection')
)
const ManufacturingSection = lazy(() =>
import('@/components/sections/ManufacturingSection').then(module => ({ default: module.default }))
)
const StatsSection = lazy(() =>
import('@/components/sections/StatsSection').then(module => ({ default: module.default }))
)
const CertificationsSection = lazy(() =>
import('@/components/sections/CertificationsSection').then(module => ({ default: module.default }))
)
const NewsSection = lazy(() =>
import('@/components/sections/NewsSection').then(module => ({ default: module.default }))
)
const OurValues = lazy(() =>
import('@/components/sections/OurValues').then(module => ({ default: module.default }))
)
// Intersection Observer for better mobile performance
const useIntersectionObserver = (ref: React.RefObject<HTMLElement | null>, threshold = 0.1) => {
const [isIntersecting, setIsIntersecting] = useState(false)
useEffect(() => {
const current = ref.current
if (!current) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsIntersecting(true)
observer.unobserve(current)
}
},
{ threshold, rootMargin: '100px' }
)
observer.observe(current)
return () => observer.disconnect()
}, [ref, threshold])
return isIntersecting
}
// Mobile-optimized lazy wrapper
function MobileLazyWrapper({
children,
fallback,
className = '',
priority = false
}: {
children: React.ReactNode
fallback: React.ReactNode
className?: string
priority?: boolean
}) {
const ref = useRef<HTMLDivElement>(null)
const isVisible = useIntersectionObserver(ref as React.RefObject<HTMLElement>)
const [shouldRender, setShouldRender] = useState(priority)
useEffect(() => {
if (isVisible || priority) {
// Add small delay for mobile to prevent blocking
const timer = setTimeout(() => setShouldRender(true), isMobile() ? 50 : 0)
return () => clearTimeout(timer)
}
}, [isVisible, priority])
return (
<div ref={ref} className={className}>
{shouldRender ? (
<Suspense fallback={fallback}>
{children}
</Suspense>
) : (
fallback
)}
</div>
)
}
// Optimized lazy components with better mobile performance
export function MobileOptimizedLazyHeroSection() {
const fallback = (
<div className="relative min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 animate-pulse">
<div className="absolute inset-0 bg-black/20"></div>
<div className="relative z-10 flex items-center justify-center min-h-screen">
<div className="text-center space-y-6 px-4">
<div className="h-16 w-96 bg-white/20 rounded-lg mx-auto"></div>
<div className="h-6 w-64 bg-white/20 rounded mx-auto"></div>
<div className="h-12 w-48 bg-white/20 rounded-lg mx-auto"></div>
</div>
</div>
</div>
)
return (
<MobileLazyWrapper fallback={fallback} priority={true}>
<HeroSection />
</MobileLazyWrapper>
)
}
export function MobileOptimizedLazyAboutSection() {
const fallback = (
<div className="py-20 bg-white animate-pulse">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center space-y-4 mb-16">
<div className="h-6 w-24 bg-gray-200 rounded mx-auto"></div>
<div className="h-8 w-64 bg-gray-200 rounded mx-auto"></div>
<div className="h-4 w-96 bg-gray-200 rounded mx-auto"></div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div className="h-64 bg-gray-200 rounded-lg"></div>
<div className="space-y-4">
<div className="h-6 w-full bg-gray-200 rounded"></div>
<div className="h-6 w-5/6 bg-gray-200 rounded"></div>
<div className="h-6 w-4/6 bg-gray-200 rounded"></div>
</div>
</div>
</div>
</div>
)
return (
<MobileLazyWrapper fallback={fallback}>
<AboutSection />
</MobileLazyWrapper>
)
}
export function MobileOptimizedLazyKashminaSection() {
const fallback = (
<div className="py-20 bg-gradient-to-br from-emerald-50 to-green-50 animate-pulse">
<div className="max-w-7xl mx-auto px-4">
<div className="text-center space-y-4 mb-16">
<div className="h-6 w-32 bg-gray-200 rounded mx-auto"></div>
<div className="h-8 w-64 bg-gray-200 rounded mx-auto"></div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
<div className="h-96 bg-gray-200 rounded-lg"></div>
<div className="space-y-6">
<div className="h-8 w-full bg-gray-200 rounded"></div>
<div className="space-y-3">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-4 w-full bg-gray-200 rounded"></div>
))}
</div>
</div>
</div>
</div>
</div>
)
return (
<MobileLazyWrapper fallback={fallback}>
<KashminaSection />
</MobileLazyWrapper>
)
}
export function MobileOptimizedLazyManufacturingSection() {
const fallback = (
<div className="py-20 bg-slate-50 animate-pulse">
<div className="max-w-7xl mx-auto px-4">
<div className="text-center space-y-4 mb-16">
<div className="h-6 w-40 bg-gray-200 rounded mx-auto"></div>
<div className="h-8 w-72 bg-gray-200 rounded mx-auto"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-64 bg-white rounded-lg shadow-sm"></div>
))}
</div>
</div>
</div>
)
return (
<MobileLazyWrapper fallback={fallback}>
<ManufacturingSection />
</MobileLazyWrapper>
)
}
export function MobileOptimizedLazyStatsSection() {
const fallback = (
<div className="py-20 bg-gradient-to-r from-blue-600 to-purple-600 animate-pulse">
<div className="max-w-7xl mx-auto px-4">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="text-center">
<div className="h-12 w-20 bg-white/20 rounded mx-auto mb-4"></div>
<div className="h-4 w-24 bg-white/20 rounded mx-auto"></div>
</div>
))}
</div>
</div>
</div>
)
return (
<MobileLazyWrapper fallback={fallback}>
<StatsSection />
</MobileLazyWrapper>
)
}
export function MobileOptimizedLazyCertificationsSection() {
const fallback = (
<div className="py-20 bg-white animate-pulse">
<div className="max-w-7xl mx-auto px-4">
<div className="text-center space-y-4 mb-16">
<div className="h-6 w-32 bg-gray-200 rounded mx-auto"></div>
<div className="h-8 w-64 bg-gray-200 rounded mx-auto"></div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-32 bg-gray-200 rounded-lg"></div>
))}
</div>
</div>
</div>
)
return (
<MobileLazyWrapper fallback={fallback}>
<CertificationsSection />
</MobileLazyWrapper>
)
}
export function MobileOptimizedLazyNewsSection() {
const fallback = (
<div className="py-20 bg-slate-50 animate-pulse">
<div className="max-w-7xl mx-auto px-4">
<div className="text-center space-y-4 mb-16">
<div className="h-6 w-32 bg-gray-200 rounded mx-auto"></div>
<div className="h-8 w-64 bg-gray-200 rounded mx-auto"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-64 bg-white rounded-lg shadow-sm"></div>
))}
</div>
</div>
</div>
)
return (
<MobileLazyWrapper fallback={fallback}>
<NewsSection />
</MobileLazyWrapper>
)
}
export function MobileOptimizedLazyOurValues() {
const fallback = (
<div className="py-20 bg-gradient-to-br from-slate-50 to-blue-50 animate-pulse">
<div className="max-w-7xl mx-auto px-4">
<div className="text-center space-y-4 mb-16">
<div className="h-6 w-24 bg-gray-200 rounded mx-auto"></div>
<div className="h-8 w-64 bg-gray-200 rounded mx-auto"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-64 bg-white rounded-lg shadow-lg"></div>
))}
</div>
</div>
</div>
)
return (
<MobileLazyWrapper fallback={fallback}>
<OurValues />
</MobileLazyWrapper>
)
}

View File

@@ -0,0 +1,224 @@
'use client'
import { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { X, Download, Smartphone } from 'lucide-react'
interface BeforeInstallPromptEvent extends Event {
prompt(): Promise<void>
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
}
export default function PWAInstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null)
const [showPrompt, setShowPrompt] = useState(false)
const [isInstalled, setIsInstalled] = useState(false)
useEffect(() => {
// Check if app is already installed
const checkInstalled = () => {
const isStandalone = window.matchMedia('(display-mode: standalone)').matches
const isInWebAppiOS = (window.navigator as any).standalone === true
const isAndroidInstalled = document.referrer.includes('android-app://')
setIsInstalled(isStandalone || isInWebAppiOS || isAndroidInstalled)
}
checkInstalled()
const handleBeforeInstallPrompt = (e: Event) => {
e.preventDefault()
setDeferredPrompt(e as BeforeInstallPromptEvent)
// Don't show prompt immediately, wait a bit for user engagement
setTimeout(() => {
if (!isInstalled && !localStorage.getItem('pwa-install-dismissed')) {
setShowPrompt(true)
}
}, 10000) // Show after 10 seconds
}
const handleAppInstalled = () => {
setShowPrompt(false)
setIsInstalled(true)
setDeferredPrompt(null)
}
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
window.addEventListener('appinstalled', handleAppInstalled)
return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
window.removeEventListener('appinstalled', handleAppInstalled)
}
}, [isInstalled])
const handleInstallClick = async () => {
if (!deferredPrompt) return
try {
await deferredPrompt.prompt()
const choiceResult = await deferredPrompt.userChoice
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the install prompt')
} else {
console.log('User dismissed the install prompt')
}
setDeferredPrompt(null)
setShowPrompt(false)
} catch (error) {
console.error('Error showing install prompt:', error)
}
}
const handleDismiss = () => {
setShowPrompt(false)
localStorage.setItem('pwa-install-dismissed', 'true')
// Show again after 7 days
setTimeout(() => {
localStorage.removeItem('pwa-install-dismissed')
}, 7 * 24 * 60 * 60 * 1000)
}
if (isInstalled || !showPrompt || !deferredPrompt) {
return null
}
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: 100 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 100 }}
className="fixed bottom-4 left-4 right-4 z-50 max-w-sm mx-auto"
>
<Card className="bg-gradient-to-r from-green-500 to-emerald-600 text-white border-0 shadow-2xl">
<CardContent className="p-4">
<div className="flex items-start space-x-3">
<div className="flex-shrink-0">
<div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center">
<Smartphone className="h-6 w-6 text-white" />
</div>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-white mb-1">
Install Padmaaja Rasooi App
</h3>
<p className="text-xs text-green-100 mb-3">
Get the full experience with offline access, faster loading, and push notifications.
</p>
<div className="flex space-x-2">
<Button
onClick={handleInstallClick}
size="sm"
variant="secondary"
className="flex-1 text-green-700 hover:text-green-800"
>
<Download className="h-3 w-3 mr-1" />
Install
</Button>
<Button
onClick={handleDismiss}
size="sm"
variant="ghost"
className="text-white hover:bg-white/20"
>
Later
</Button>
</div>
</div>
<Button
onClick={handleDismiss}
size="sm"
variant="ghost"
className="flex-shrink-0 text-white hover:bg-white/20 p-1"
>
<X className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
</motion.div>
</AnimatePresence>
)
}
// Alternative iOS Safari install instructions
export function IOSInstallPrompt() {
const [showIOS, setShowIOS] = useState(false)
useEffect(() => {
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
const isInStandaloneMode = (window.navigator as any).standalone === true
const hasPromptBeenShown = localStorage.getItem('ios-install-prompt-shown')
if (isIOS && !isInStandaloneMode && !hasPromptBeenShown) {
setTimeout(() => setShowIOS(true), 15000) // Show after 15 seconds on iOS
}
}, [])
const handleDismiss = () => {
setShowIOS(false)
localStorage.setItem('ios-install-prompt-shown', 'true')
}
if (!showIOS) return null
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: 100 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 100 }}
className="fixed bottom-4 left-4 right-4 z-50 max-w-sm mx-auto"
>
<Card className="bg-blue-600 text-white border-0 shadow-2xl">
<CardContent className="p-4">
<div className="flex items-start space-x-3">
<div className="flex-shrink-0">
<div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center">
<Smartphone className="h-6 w-6 text-white" />
</div>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-white mb-1">
Add to Home Screen
</h3>
<p className="text-xs text-blue-100 mb-2">
Tap the share button <span className="inline-block w-4 h-4 bg-white/20 rounded text-center text-xs"></span> below and select "Add to Home Screen"
</p>
<Button
onClick={handleDismiss}
size="sm"
variant="secondary"
className="text-blue-700 hover:text-blue-800"
>
Got it
</Button>
</div>
<Button
onClick={handleDismiss}
size="sm"
variant="ghost"
className="flex-shrink-0 text-white hover:bg-white/20 p-1"
>
<X className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
</motion.div>
</AnimatePresence>
)
}

443
components/ProductCard.tsx Normal file
View File

@@ -0,0 +1,443 @@
'use client'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { ShoppingCart, Star, Eye, X } from 'lucide-react'
import Link from 'next/link'
import OptimizedImage from '@/components/ui/OptimizedImage'
import { motion } from 'framer-motion'
import { cartManager } from '@/lib/cart'
import { toast } from 'sonner'
import { useRouter } from 'next/navigation'
import { Product } from '@/types'
import { useState, useEffect } from 'react'
interface ProductReviewStats {
averageRating: number
totalReviews: number
}
interface ProductCardProps {
product: Product
index: number
}
export default function ProductCard({ product, index }: ProductCardProps) {
const router = useRouter()
const [isQuickViewOpen, setIsQuickViewOpen] = useState(false)
const [reviewStats, setReviewStats] = useState<ProductReviewStats>({
averageRating: 0,
totalReviews: 0
})
// Fetch review statistics for the product
useEffect(() => {
const fetchReviewStats = async () => {
try {
const response = await fetch(`/api/reviews?productId=${product.id}&limit=1000`)
const data = await response.json()
if (response.ok && data.reviews) {
const reviews = data.reviews
const totalReviews = reviews.length
if (totalReviews > 0) {
const sum = reviews.reduce((acc: number, review: any) => acc + review.rating, 0)
const averageRating = sum / totalReviews
setReviewStats({
averageRating: Math.round(averageRating * 10) / 10, // Round to 1 decimal
totalReviews
})
}
}
} catch (error) {
console.error('Failed to fetch review stats:', error)
}
}
fetchReviewStats()
}, [product.id])
const getDiscountedPrice = (price: number, discount: number) => {
return price - (price * discount / 100)
}
// Helper function to calculate per kg price
const getPerKgPrice = (price: number, weight: string | null, discount: number = 0) => {
if (!weight) return null
// Extract numeric value from weight string (e.g., "1kg", "500g", "2.5 kg")
const weightMatch = weight.toLowerCase().match(/(\d+(?:\.\d+)?)\s*(kg|g|gram|kilos?)/i)
if (!weightMatch) return null
const value = parseFloat(weightMatch[1])
const unit = weightMatch[2].toLowerCase()
// Convert to kg
let weightInKg = value
if (unit.startsWith('g')) {
weightInKg = value / 1000
}
const finalPrice = discount > 0 ? getDiscountedPrice(price, discount) : price
return Math.round(finalPrice / weightInKg)
}
const handleAddToCart = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
console.log('Add to cart clicked for:', product.name) // Debug log
if (product.stock === 0) {
toast.error('Product is out of stock')
return
}
try {
const success = cartManager.addToCart(product, 1)
if (success) {
console.log('Item added successfully') // Debug log
toast.success(`${product.name} added to cart!`)
} else {
toast.error('Not enough stock available')
}
} catch (error) {
console.error('Error adding to cart:', error)
toast.error('Failed to add to cart')
}
}
const handleCardClick = () => {
router.push(`/products/${product.slug}`)
}
const handleViewDetails = (e: React.MouseEvent) => {
e.stopPropagation()
setIsQuickViewOpen(true)
}
return (
<article
itemScope
itemType="https://schema.org/Product"
role="article"
aria-labelledby={`product-title-${product.id}`}
aria-describedby={`product-description-${product.id}`}
>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="h-full"
>
<Card
className="h-full hover:shadow-lg transition-all duration-300 group overflow-hidden cursor-pointer hover:scale-[1.02]"
onClick={handleCardClick}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleCardClick()
}
}}
aria-label={`View details for ${product.name}`}
>
<header className="relative overflow-hidden">
<div itemProp="image">
<OptimizedImage
src={product.images[0] || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={`${product.name} - Premium rice product from Padmaaja Rasooi`}
width={400}
height={300}
className="w-full h-full object-contain group-hover:scale-105 transition-transform duration-300"
priority={index < 4} // Prioritize first 4 products
quality={85}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 25vw"
/>
</div>
{/* Per kg price badge - Top Left */}
{getPerKgPrice(product.price, product.weight, product.discount) && (
<Badge className="absolute top-2 left-2 sm:top-3 sm:left-3 bg-emerald-500 hover:bg-emerald-600 text-white font-semibold shadow-lg text-base">
{getPerKgPrice(product.price, product.weight, product.discount)}/kg
</Badge>
)}
{/* Discount badge - Top Left, below per kg badge if both exist */}
{product.discount > 0 && (
<Badge className={`absolute ${getPerKgPrice(product.price, product.weight, product.discount) ? 'top-8 sm:top-10' : 'top-2 sm:top-3'} left-2 sm:left-3 bg-red-500 hover:bg-red-600 text-white font-semibold shadow-lg text-xs`}>
{product.discount}% OFF
</Badge>
)}
{/* Dynamic Rating Display */}
{reviewStats.totalReviews > 0 ? (
<div className="absolute top-2 right-2 sm:top-3 sm:right-3 flex items-center bg-white/90 backdrop-blur-sm rounded-full px-2 py-1 shadow-lg">
<Star className="h-3 w-3 text-yellow-400 fill-current" />
<span className="text-xs ml-1 font-medium">{reviewStats.averageRating}</span>
<span className="text-xs ml-1 text-gray-500">({reviewStats.totalReviews})</span>
</div>
) : (
<div className="absolute top-2 right-2 sm:top-3 sm:right-3 flex items-center bg-white/90 backdrop-blur-sm rounded-full px-2 py-1 shadow-lg">
<Star className="h-3 w-3 text-gray-300" />
<span className="text-xs ml-1 text-gray-400">No reviews</span>
</div>
)}
{product.stock === 0 && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<Badge variant="destructive" className="text-xs sm:text-sm">Out of Stock</Badge>
</div>
)}
<Badge className="absolute bottom-2 right-2 sm:bottom-3 sm:right-3 bg-white/90 text-slate-700 border border-slate-200 text-xs font-medium shadow-lg backdrop-blur-sm">
{product.category.name}
</Badge>
</header>
<CardHeader className="pb-2 p-3 sm:pb-2" role="contentinfo">
<div className="flex items-start justify-between gap-2 mb-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<CardTitle
className="text-base sm:text-lg font-semibold truncate group-hover:text-blue-600 transition-colors flex-1 leading-tight cursor-help"
id={`product-title-${product.id}`}
itemProp="name"
>
{product.name}
</CardTitle>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p>{product.name}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<CardDescription
className="line-clamp-2 md:text-xs text-sm text-gray-600 leading-relaxed"
id={`product-description-${product.id}`}
itemProp="description"
>
{product.description || 'Premium quality product from Padmaaja Rasooi Pvt. Ltd.'}
</CardDescription>
</CardHeader>
<CardContent className="pt-0 p-3 sm:p-6 sm:pt-0 space-y-3 sm:space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
{product.discount > 0 ? (
<>
<span className="text-base sm:text-lg font-bold text-green-600">
{getDiscountedPrice(product.price, product.discount).toFixed(2)}
</span>
<span className="text-xs sm:text-sm text-gray-500 line-through">
{product.price.toFixed(2)}
</span>
</>
) : (
<span className="text-base sm:text-lg font-bold text-gray-900">
{product.price.toFixed(2)}
</span>
)}
</div>
{/* <span className="text-xs sm:text-sm text-gray-500">
Stock: <span className={product.stock > 0 ? 'text-green-600' : 'text-red-600'}>{product.stock}</span>
</span> */}
</div>
<div className="flex gap-2">
<Button
size="sm"
onClick={handleAddToCart}
disabled={product.stock === 0}
className="flex-1 text-xs sm:text-sm h-8 sm:h-9"
>
<ShoppingCart className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2" />
<span className="hidden sm:inline">{product.stock === 0 ? 'Out of Stock' : 'Add to Cart'}</span>
<span className="sm:hidden">{product.stock === 0 ? 'Out' : 'Add'}</span>
</Button>
<Button
size="sm"
variant="outline"
onClick={handleViewDetails}
className="px-2 sm:px-3 h-8 sm:h-9"
>
<Eye className="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</div>
</CardContent>
</Card>
{/* Quick View Dialog */}
<Dialog open={isQuickViewOpen} onOpenChange={setIsQuickViewOpen}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-xl font-bold">{product.name}</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Product Image */}
<div className="space-y-4">
<div className="relative aspect-square overflow-hidden rounded-lg border">
<OptimizedImage
src={product.images[0] || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={product.name}
width={400}
height={400}
className="w-full h-full object-contain"
quality={80}
/>
{product.discount > 0 && (
<Badge className="absolute top-4 left-4 bg-red-500 hover:bg-red-600 text-white font-semibold">
{product.discount}% OFF
</Badge>
)}
</div>
{/* Additional Images if available */}
{product.images.length > 1 && (
<div className="grid grid-cols-4 gap-2">
{product.images.slice(1, 5).map((image, idx) => (
<div key={idx} className="aspect-square rounded border overflow-hidden">
<OptimizedImage
src={image}
alt={`${product.name} ${idx + 2}`}
width={100}
height={100}
className="w-full h-full object-contain"
quality={60}
/>
</div>
))}
</div>
)}
</div>
{/* Product Details */}
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-2">Product Details</h3>
<p className="text-gray-600 leading-relaxed">
{product.description || 'Premium quality product from Padmaaja Rasooi Pvt. Ltd.'}
</p>
</div>
{/* Category and Brand */}
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-sm font-medium text-gray-500">Category</span>
<p className="font-semibold">{product.category?.name}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">Brand</span>
<p className="font-semibold">{product.brand || 'Padmaaja Rasooi'}</p>
</div>
{product.weight && (
<div>
<span className="text-sm font-medium text-gray-500">Weight</span>
<p className="font-semibold">{product.weight}</p>
</div>
)}
{product.origin && (
<div>
<span className="text-sm font-medium text-gray-500">Origin</span>
<p className="font-semibold">{product.origin}</p>
</div>
)}
</div>
{/* Pricing */}
<div className="border-t pt-4">
<div className="flex items-center space-x-3 mb-4">
{product.discount > 0 ? (
<>
<span className="text-2xl font-bold text-green-600">
{getDiscountedPrice(product.price, product.discount).toFixed(2)}
</span>
<span className="text-lg text-gray-500 line-through">
{product.price.toFixed(2)}
</span>
</>
) : (
<span className="text-2xl font-bold text-gray-900">
{product.price.toFixed(2)}
</span>
)}
</div>
{/* Per kg price in quick view */}
{getPerKgPrice(product.price, product.weight, product.discount) && (
<div className="mb-4 text-sm text-gray-600">
<span className="font-medium">Per kg: </span>
{getPerKgPrice(product.price, product.weight, product.discount)}/kg
</div>
)}
{/* Stock Status */}
<div className="mb-4">
<span className="text-sm font-medium text-gray-500">Availability: </span>
<span className={`font-semibold ${product.stock > 0 ? 'text-green-600' : 'text-red-600'}`}>
{product.stock > 0 ? `In Stock (${product.stock} available)` : 'Out of Stock'}
</span>
</div>
{/* Action Buttons */}
<div className="space-y-3">
<Button
onClick={handleAddToCart}
disabled={product.stock === 0}
className="w-full"
size="lg"
>
<ShoppingCart className="h-4 w-4 mr-2" />
{product.stock === 0 ? 'Out of Stock' : 'Add to Cart'}
</Button>
<Button
variant="outline"
onClick={() => {
setIsQuickViewOpen(false)
router.push(`/products/${product.slug}`)
}}
className="w-full"
size="lg"
>
View Full Details
</Button>
</div>
</div>
{/* Dynamic Rating in Quick View */}
<div className="flex items-center space-x-2 pt-4 border-t">
<div className="flex items-center">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`h-4 w-4 ${
reviewStats.totalReviews > 0 && i < Math.round(reviewStats.averageRating)
? 'text-yellow-400 fill-current'
: 'text-gray-300'
}`}
/>
))}
</div>
<span className="text-sm text-gray-600">
{reviewStats.totalReviews > 0
? `${reviewStats.averageRating} (${reviewStats.totalReviews} reviews)`
: 'No reviews yet'
}
</span>
</div>
</div>
</div>
</DialogContent>
</Dialog>
</motion.div>
</article>
)
}

575
components/RecipeDialog.tsx Normal file
View File

@@ -0,0 +1,575 @@
'use client'
import { useState, useEffect } from 'react'
import { Recipe } from '@/lib/recipe-data'
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { Progress } from '@/components/ui/progress'
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
import {
Clock,
Users,
ChefHat,
Lightbulb,
X,
Heart,
Share2,
Printer,
Check,
CheckCircle2,
PlayCircle,
Pause,
RotateCcw,
Timer
} from 'lucide-react'
import Image from 'next/image'
import { motion, AnimatePresence } from 'framer-motion'
import { cn } from '@/lib/utils'
interface RecipeDialogProps {
recipe: Recipe | null
isOpen: boolean
onClose: () => void
}
export default function RecipeDialog({ recipe, isOpen, onClose }: RecipeDialogProps) {
const [activeStep, setActiveStep] = useState(0)
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set())
const [checkedIngredients, setCheckedIngredients] = useState<Set<number>>(new Set())
const [isTimerRunning, setIsTimerRunning] = useState(false)
const [timerTime, setTimerTime] = useState(0)
const [isFavorited, setIsFavorited] = useState(false)
const [activeTab, setActiveTab] = useState<'overview' | 'ingredients' | 'instructions' | 'tips'>('overview')
// Timer functionality
useEffect(() => {
let interval: NodeJS.Timeout
if (isTimerRunning && timerTime > 0) {
interval = setInterval(() => {
setTimerTime(time => {
if (time <= 1) {
setIsTimerRunning(false)
// Could add notification here
return 0
}
return time - 1
})
}, 1000)
}
return () => clearInterval(interval)
}, [isTimerRunning, timerTime])
// Reset state when dialog opens/closes
useEffect(() => {
if (!isOpen) {
setActiveStep(0)
setCompletedSteps(new Set())
setCheckedIngredients(new Set())
setIsTimerRunning(false)
setTimerTime(0)
setActiveTab('overview')
}
}, [isOpen])
if (!recipe) return 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 toggleIngredientCheck = (index: number) => {
const newChecked = new Set(checkedIngredients)
if (newChecked.has(index)) {
newChecked.delete(index)
} else {
newChecked.add(index)
}
setCheckedIngredients(newChecked)
}
const toggleStepComplete = (index: number) => {
const newCompleted = new Set(completedSteps)
if (newCompleted.has(index)) {
newCompleted.delete(index)
} else {
newCompleted.add(index)
}
setCompletedSteps(newCompleted)
}
const startTimer = (minutes: number) => {
setTimerTime(minutes * 60)
setIsTimerRunning(true)
}
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}:${secs.toString().padStart(2, '0')}`
}
const progress = recipe.instructions.length > 0 ? (completedSteps.size / recipe.instructions.length) * 100 : 0
const ingredientProgress = recipe.ingredients.length > 0 ? (checkedIngredients.size / recipe.ingredients.length) * 100 : 0
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="w-screen h-screen max-w-none max-h-none p-0 rounded-none border-0 bg-white overflow-hidden [&>button]:hidden">
<VisuallyHidden>
<DialogTitle>{recipe.name} - Recipe Details</DialogTitle>
</VisuallyHidden>
<div className="flex flex-col h-full overflow-hidden">
{/* Header */}
<div className="relative flex-shrink-0">
{/* Hero Image */}
<div className="relative h-48 sm:h-56 lg:h-64 overflow-hidden">
<Image
src={recipe.image}
alt={recipe.name}
fill
className="object-cover"
sizes="100vw"
priority
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent" />
{/* Close Button */}
<Button
variant="ghost"
size="icon"
onClick={onClose}
className="absolute top-4 right-4 bg-black/20 backdrop-blur-sm text-white hover:bg-black/40 rounded-full w-10 h-10"
>
<X className="h-5 w-5" />
</Button>
{/* Action Buttons */}
<div className="absolute top-4 left-4 flex gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => setIsFavorited(!isFavorited)}
className={cn(
"bg-black/20 backdrop-blur-sm rounded-full w-10 h-10 transition-colors",
isFavorited ? "text-red-500 hover:bg-red-500/20" : "text-white hover:bg-black/40"
)}
>
<Heart className={cn("h-5 w-5", isFavorited && "fill-current")} />
</Button>
<Button
variant="ghost"
size="icon"
className="bg-black/20 backdrop-blur-sm text-white hover:bg-black/40 rounded-full w-10 h-10"
>
<Share2 className="h-5 w-5" />
</Button>
<Button
variant="ghost"
size="icon"
className="bg-black/20 backdrop-blur-sm text-white hover:bg-black/40 rounded-full w-10 h-10"
>
<Printer className="h-5 w-5" />
</Button>
</div>
{/* Title Overlay */}
<div className="absolute bottom-4 left-4 right-4">
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-white mb-1 drop-shadow-lg">
{recipe.name}
</h1>
<p className="text-white/90 text-base sm:text-lg drop-shadow-lg max-w-2xl line-clamp-2">
{recipe.description}
</p>
</div>
</div>
{/* Recipe Meta Info */}
<div className="bg-white border-b border-gray-200 p-4 flex-shrink-0">
<div className="max-w-7xl mx-auto">
<div className="flex flex-wrap gap-4 items-center justify-between mb-3">
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-amber-600" />
<span className="text-sm font-medium">{recipe.cookTime}</span>
</div>
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-blue-600" />
<span className="text-sm font-medium">{recipe.servings} servings</span>
</div>
<div className="flex items-center gap-2">
<ChefHat className="w-4 h-4 text-purple-600" />
<Badge className={getDifficultyColor(recipe.difficulty)}>
{recipe.difficulty}
</Badge>
</div>
<Badge variant="outline" className="text-orange-700 border-orange-200 text-xs">
{recipe.category}
</Badge>
</div>
{/* Timer */}
{timerTime > 0 && (
<div className="flex items-center gap-2 bg-orange-50 px-3 py-1.5 rounded-lg border border-orange-200">
<Timer className="w-4 h-4 text-orange-600" />
<span className="font-mono text-sm font-medium text-orange-800">
{formatTime(timerTime)}
</span>
<Button
variant="ghost"
size="sm"
onClick={() => setIsTimerRunning(!isTimerRunning)}
className="p-1 h-auto"
>
{isTimerRunning ? <Pause className="w-3 h-3" /> : <PlayCircle className="w-3 h-3" />}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setTimerTime(0)}
className="p-1 h-auto"
>
<RotateCcw className="w-3 h-3" />
</Button>
</div>
)}
</div>
{/* Progress Bars */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<div className="flex justify-between text-xs text-gray-600 mb-1">
<span>Ingredients checked</span>
<span>{checkedIngredients.size}/{recipe.ingredients.length}</span>
</div>
<Progress value={ingredientProgress} className="h-1.5" />
</div>
<div>
<div className="flex justify-between text-xs text-gray-600 mb-1">
<span>Steps completed</span>
<span>{completedSteps.size}/{recipe.instructions.length}</span>
</div>
<Progress value={progress} className="h-1.5" />
</div>
</div>
</div>
</div>
{/* Tab Navigation */}
<div className="bg-white border-b border-gray-200 flex-shrink-0">
<div className="max-w-7xl mx-auto px-4">
<nav className="flex space-x-6">
{[
{ id: 'overview', label: 'Overview', icon: ChefHat },
{ id: 'ingredients', label: 'Ingredients', icon: Check },
{ id: 'instructions', label: 'Instructions', icon: PlayCircle },
{ id: 'tips', label: 'Tips', icon: Lightbulb }
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={cn(
"flex items-center gap-2 py-3 px-1 border-b-2 font-medium text-sm transition-colors",
activeTab === tab.id
? "border-orange-500 text-orange-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
)}
>
<tab.icon className="w-4 h-4" />
{tab.label}
</button>
))}
</nav>
</div>
</div>
</div>
{/* Main Content */}
<div className="flex-1 overflow-y-auto recipe-dialog-scroll">
<div className="max-w-7xl mx-auto p-4">
<AnimatePresence mode="wait">
{activeTab === 'overview' && (
<motion.div
key="overview"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
className="space-y-8"
>
{/* Nutrition Info */}
{recipe.nutritionInfo && (
<div className="bg-gradient-to-r from-green-50 to-blue-50 rounded-xl p-6 border border-green-100">
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center gap-2">
<div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-bold">📊</span>
</div>
Nutrition Information (per serving)
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
<div className="text-center bg-white rounded-lg p-4 shadow-sm">
<div className="text-2xl font-bold text-orange-600">{recipe.nutritionInfo.calories}</div>
<div className="text-gray-600 font-medium">Calories</div>
</div>
<div className="text-center bg-white rounded-lg p-4 shadow-sm">
<div className="text-2xl font-bold text-blue-600">{recipe.nutritionInfo.protein}</div>
<div className="text-gray-600 font-medium">Protein</div>
</div>
<div className="text-center bg-white rounded-lg p-4 shadow-sm">
<div className="text-2xl font-bold text-green-600">{recipe.nutritionInfo.carbs}</div>
<div className="text-gray-600 font-medium">Carbs</div>
</div>
<div className="text-center bg-white rounded-lg p-4 shadow-sm">
<div className="text-2xl font-bold text-purple-600">{recipe.nutritionInfo.fat}</div>
<div className="text-gray-600 font-medium">Fat</div>
</div>
</div>
</div>
)}
{/* Quick Actions */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Button
variant="outline"
onClick={() => startTimer(5)}
className="h-20 flex flex-col gap-2 hover:bg-orange-50 hover:border-orange-200"
>
<Timer className="w-6 h-6 text-orange-600" />
<span className="text-sm">5 min timer</span>
</Button>
<Button
variant="outline"
onClick={() => startTimer(10)}
className="h-20 flex flex-col gap-2 hover:bg-orange-50 hover:border-orange-200"
>
<Timer className="w-6 h-6 text-orange-600" />
<span className="text-sm">10 min timer</span>
</Button>
<Button
variant="outline"
onClick={() => startTimer(15)}
className="h-20 flex flex-col gap-2 hover:bg-orange-50 hover:border-orange-200"
>
<Timer className="w-6 h-6 text-orange-600" />
<span className="text-sm">15 min timer</span>
</Button>
<Button
variant="outline"
onClick={() => setActiveTab('ingredients')}
className="h-20 flex flex-col gap-2 hover:bg-green-50 hover:border-green-200"
>
<Check className="w-6 h-6 text-green-600" />
<span className="text-sm">Start cooking</span>
</Button>
</div>
</motion.div>
)}
{activeTab === 'ingredients' && (
<motion.div
key="ingredients"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<div className="bg-green-50 p-6 border-b border-green-100">
<h3 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
<div className="w-10 h-10 bg-green-500 rounded-full flex items-center justify-center">
<span className="text-white text-lg">🥘</span>
</div>
Ingredients
</h3>
<p className="text-gray-600 mt-2">Check off ingredients as you gather them</p>
</div>
<div className="p-6">
<div className="space-y-4">
{recipe.ingredients.map((ingredient, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className={cn(
"flex items-center gap-4 p-4 rounded-lg border-2 transition-all cursor-pointer hover:shadow-md",
checkedIngredients.has(index)
? "bg-green-50 border-green-200 text-green-800"
: "bg-white border-gray-200 hover:border-gray-300"
)}
onClick={() => toggleIngredientCheck(index)}
>
<div className={cn(
"w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all",
checkedIngredients.has(index)
? "bg-green-500 border-green-500"
: "border-gray-300"
)}>
{checkedIngredients.has(index) && (
<Check className="w-4 h-4 text-white" />
)}
</div>
<span className={cn(
"text-lg leading-relaxed flex-1",
checkedIngredients.has(index) && "line-through"
)}>
{ingredient}
</span>
</motion.div>
))}
</div>
</div>
</div>
</motion.div>
)}
{activeTab === 'instructions' && (
<motion.div
key="instructions"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<div className="bg-blue-50 p-6 border-b border-blue-100">
<h3 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
<div className="w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center">
<span className="text-white text-lg">📝</span>
</div>
Cooking Instructions
</h3>
<p className="text-gray-600 mt-2">Follow these steps to create your delicious meal</p>
</div>
<div className="p-6">
<div className="space-y-6">
{recipe.instructions.map((instruction, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className={cn(
"flex gap-6 p-6 rounded-xl border-2 transition-all",
completedSteps.has(index)
? "bg-green-50 border-green-200"
: activeStep === index
? "bg-blue-50 border-blue-200 shadow-lg"
: "bg-white border-gray-200"
)}
>
<div className="flex flex-col items-center gap-3">
<button
onClick={() => toggleStepComplete(index)}
className={cn(
"w-12 h-12 rounded-full font-bold text-lg transition-all flex items-center justify-center",
completedSteps.has(index)
? "bg-green-500 text-white"
: activeStep === index
? "bg-blue-500 text-white"
: "bg-gray-200 text-gray-600 hover:bg-gray-300"
)}
>
{completedSteps.has(index) ? (
<CheckCircle2 className="w-6 h-6" />
) : (
index + 1
)}
</button>
<Button
variant="ghost"
size="sm"
onClick={() => setActiveStep(index)}
className={cn(
"text-xs",
activeStep === index ? "text-blue-600" : "text-gray-500"
)}
>
{activeStep === index ? "Current" : "Go to"}
</Button>
</div>
<div className="flex-1">
<p className={cn(
"text-lg leading-relaxed",
completedSteps.has(index) && "line-through text-gray-500"
)}>
{instruction}
</p>
{index === activeStep && (
<div className="mt-4 flex gap-2">
<Button
size="sm"
onClick={() => startTimer(5)}
variant="outline"
className="text-orange-600 border-orange-200 hover:bg-orange-50"
>
<Timer className="w-4 h-4 mr-1" />
5 min
</Button>
<Button
size="sm"
onClick={() => startTimer(10)}
variant="outline"
className="text-orange-600 border-orange-200 hover:bg-orange-50"
>
<Timer className="w-4 h-4 mr-1" />
10 min
</Button>
</div>
)}
</div>
</motion.div>
))}
</div>
</div>
</div>
</motion.div>
)}
{activeTab === 'tips' && recipe.tips && recipe.tips.length > 0 && (
<motion.div
key="tips"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
<div className="bg-gradient-to-r from-yellow-50 to-orange-50 rounded-xl border border-yellow-200 overflow-hidden">
<div className="bg-yellow-100 p-6 border-b border-yellow-200">
<h3 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
<Lightbulb className="w-8 h-8 text-yellow-600" />
Chef's Tips & Tricks
</h3>
<p className="text-gray-600 mt-2">Professional insights to elevate your cooking</p>
</div>
<div className="p-6">
<div className="space-y-4">
{recipe.tips.map((tip, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="flex items-start gap-4 p-4 bg-white rounded-lg border border-yellow-200 shadow-sm"
>
<span className="text-2xl">💡</span>
<p className="text-lg leading-relaxed text-gray-700 flex-1">{tip}</p>
</motion.div>
))}
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
</DialogContent>
</Dialog>
)
}

137
components/SEO.tsx Normal file
View File

@@ -0,0 +1,137 @@
import Head from 'next/head'
interface SEOProps {
title?: string
description?: string
keywords?: string[]
image?: string
url?: string
type?: 'website' | 'article' | 'product'
publishedTime?: string
modifiedTime?: string
author?: string
siteName?: string
locale?: string
}
export default function SEO({
title = 'Padmaaja Rasooi - Premium Basmati & Sella Rice | Aged Quality Rice',
description = 'Experience authentic aged Basmati 1121 and premium Sella rice from Padmaaja Rasooi. Direct from Punjab & Haryana rice belt with traditional aging process.',
keywords = ['padmaaja rasooi', 'basmati rice', 'aged basmati', 'sella rice', 'premium rice', 'rice export', 'kashmina rice', '1121 basmati'],
image = '/images/og-image.png',
url = '',
type = 'website',
publishedTime,
modifiedTime,
author = 'Padmaaja Rasooi Team',
siteName = 'Padmaaja Rasooi',
locale = 'en_US'
}: SEOProps) {
const baseUrl = process.env.NEXT_PUBLIC_URL || 'https://padmaajarasooi.com'
const fullUrl = url ? `${baseUrl}${url}` : baseUrl
const fullImageUrl = image.startsWith('http') ? image : `${baseUrl}${image}`
return (
<Head>
{/* Basic Meta Tags */}
<title>{title}</title>
<meta name="description" content={description} />
<meta name="keywords" content={keywords.join(', ')} />
<meta name="author" content={author} />
<link rel="canonical" href={fullUrl} />
{/* Open Graph Meta Tags */}
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={fullImageUrl} />
<meta property="og:url" content={fullUrl} />
<meta property="og:type" content={type} />
<meta property="og:site_name" content={siteName} />
<meta property="og:locale" content={locale} />
{/* Article specific */}
{type === 'article' && publishedTime && (
<meta property="article:published_time" content={publishedTime} />
)}
{type === 'article' && modifiedTime && (
<meta property="article:modified_time" content={modifiedTime} />
)}
{type === 'article' && author && (
<meta property="article:author" content={author} />
)}
{/* Twitter Card Meta Tags */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={fullImageUrl} />
<meta name="twitter:site" content="@padmaajarasooi" />
<meta name="twitter:creator" content="@padmaajarasooi" />
{/* Additional SEO Meta Tags */}
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
<meta name="googlebot" content="index, follow" />
<meta name="bingbot" content="index, follow" />
{/* Mobile Optimization */}
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5" />
<meta name="format-detection" content="telephone=no" />
{/* Language and Region */}
<meta httpEquiv="content-language" content="en" />
<meta name="geo.region" content="IN" />
<meta name="geo.country" content="India" />
{/* Schema.org for Google */}
<meta itemProp="name" content={title} />
<meta itemProp="description" content={description} />
<meta itemProp="image" content={fullImageUrl} />
</Head>
)
}
// Pre-defined SEO configurations for common pages
export const SEOConfigs = {
home: {
title: 'Padmaaja Rasooi - Premium Basmati & Sella Rice | Aged Quality Rice',
description: 'Experience authentic aged Basmati 1121 and premium Sella rice from Padmaaja Rasooi. Direct from Punjab & Haryana rice belt with traditional aging process.',
keywords: ['padmaaja rasooi', 'basmati rice', 'aged basmati', 'sella rice', 'premium rice', 'rice export', 'kashmina rice', '1121 basmati', 'rice trade'],
url: '/'
},
products: {
title: 'Premium Rice Products | Padmaaja Rasooi',
description: 'Explore our extensive collection of premium quality rice varieties. From organic basmati to traditional rice, find the perfect rice for your needs.',
keywords: ['premium rice', 'organic rice', 'basmati rice', 'rice varieties', 'quality rice', 'rice products', 'padmaaja rasooi'],
url: '/products'
},
about: {
title: 'About Us | Padmaaja Rasooi - Quality Rice Heritage',
description: 'Learn about Padmaaja Rasooi\'s commitment to quality rice products and sustainable farming practices. Discover our story and values.',
keywords: ['about padmaaja rasooi', 'rice company', 'quality standards', 'sustainable farming', 'rice heritage'],
url: '/about'
},
contact: {
title: 'Contact Us | Padmaaja Rasooi',
description: 'Get in touch with Padmaaja Rasooi for premium rice products, wholesale inquiries, or any questions. We\'re here to help.',
keywords: ['contact padmaaja rasooi', 'rice supplier contact', 'wholesale inquiry', 'customer support'],
url: '/contact'
},
privacyPolicy: {
title: 'Privacy Policy | Padmaaja Rasooi',
description: 'Read our privacy policy to understand how Padmaaja Rasooi protects and handles your personal information.',
keywords: ['privacy policy', 'data protection', 'personal information', 'padmaaja rasooi'],
url: '/legal/privacy-policy'
},
termsOfService: {
title: 'Terms of Service | Padmaaja Rasooi',
description: 'Read our terms of service for using Padmaaja Rasooi products and services.',
keywords: ['terms of service', 'legal terms', 'service conditions', 'padmaaja rasooi'],
url: '/legal/terms-of-service'
},
refundPolicy: {
title: 'Refund Policy | Padmaaja Rasooi',
description: 'Learn about our refund and return policy for Padmaaja Rasooi rice products and services.',
keywords: ['refund policy', 'return policy', 'money back guarantee', 'padmaaja rasooi'],
url: '/legal/refund-policy'
}
}

View File

@@ -0,0 +1,44 @@
'use client'
import { Thing, WithContext } from 'schema-dts'
interface StructuredDataProps {
data: WithContext<Thing> | any
id?: string
}
/**
* StructuredData component for rendering JSON-LD
* Sanitizes output to prevent XSS attacks by replacing < with \u003c
*/
export default function StructuredData({ data, id }: StructuredDataProps) {
return (
<script
id={id || 'structured-data'}
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(data).replace(/</g, '\\u003c')
}}
/>
)
}
// Utility component for multiple structured data objects
interface MultipleStructuredDataProps {
dataArray: Array<WithContext<Thing> | any>
idPrefix?: string
}
export function MultipleStructuredData({ dataArray, idPrefix = 'structured-data' }: MultipleStructuredDataProps) {
return (
<>
{dataArray.map((data, index) => (
<StructuredData
key={`${idPrefix}-${index}`}
id={`${idPrefix}-${index}`}
data={data}
/>
))}
</>
)
}

View File

@@ -0,0 +1,182 @@
'use client'
import { useEffect, useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { RefreshCw, X, Download } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { registerUpdateHandler, forceReload } from '@/lib/cache-manager'
interface UpdateNotificationProps {
autoShow?: boolean
position?: 'top' | 'bottom'
}
export default function UpdateNotification({
autoShow = true,
position = 'bottom'
}: UpdateNotificationProps) {
const [showUpdate, setShowUpdate] = useState(false)
const [isUpdating, setIsUpdating] = useState(false)
useEffect(() => {
if (autoShow) {
// Register for update notifications
registerUpdateHandler(() => {
setShowUpdate(true)
})
}
}, [autoShow])
const handleUpdate = async () => {
setIsUpdating(true)
try {
// Force reload with cache clear
forceReload()
} catch (error) {
console.error('Update failed:', error)
setIsUpdating(false)
}
}
const handleDismiss = () => {
setShowUpdate(false)
}
const positionStyles = position === 'top'
? 'top-4 left-4 right-4'
: 'bottom-4 left-4 right-4'
return (
<AnimatePresence>
{showUpdate && (
<motion.div
initial={{ opacity: 0, y: position === 'top' ? -100 : 100 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: position === 'top' ? -100 : 100 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
className={`fixed ${positionStyles} z-[9999] max-w-md mx-auto`}
>
<Card className="shadow-2xl border-0 bg-gradient-to-r from-emerald-500 to-blue-500 text-white overflow-hidden">
<CardContent className="p-0">
<div className="relative">
{/* Background pattern */}
<div className="absolute inset-0 bg-gradient-to-r from-emerald-600/20 to-blue-600/20" />
{/* Content */}
<div className="relative p-4">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center space-x-2">
<div className="p-2 bg-white/20 rounded-lg backdrop-blur-sm">
<Download className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="font-semibold text-white">
Update Available!
</h3>
<Badge variant="secondary" className="bg-white/20 text-white text-xs mt-1">
Version 2.0
</Badge>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={handleDismiss}
className="text-white hover:bg-white/20 p-1 h-auto"
disabled={isUpdating}
>
<X className="w-4 h-4" />
</Button>
</div>
<p className="text-white/90 text-sm mb-4 leading-relaxed">
A new version is available with improved features and bug fixes.
Refresh to get the latest updates.
</p>
<div className="flex gap-2">
<Button
onClick={handleUpdate}
disabled={isUpdating}
size="sm"
className="bg-white text-emerald-600 hover:bg-white/90 flex-1"
>
{isUpdating ? (
<>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
>
<RefreshCw className="w-4 h-4 mr-2" />
</motion.div>
Updating...
</>
) : (
<>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh Now
</>
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleDismiss}
disabled={isUpdating}
className="text-white hover:bg-white/20"
>
Later
</Button>
</div>
</div>
{/* Animated shimmer effect */}
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent"
animate={{
x: ['-100%', '100%']
}}
transition={{
duration: 2,
repeat: Infinity,
ease: 'linear'
}}
/>
</div>
</CardContent>
</Card>
</motion.div>
)}
</AnimatePresence>
)
}
// Development component to manually trigger updates
export function DevUpdateTrigger() {
const [showUpdate, setShowUpdate] = useState(false)
if (process.env.NODE_ENV !== 'development') {
return null
}
return (
<div className="fixed top-20 right-4 z-[9999]">
<Button
onClick={() => setShowUpdate(!showUpdate)}
size="sm"
variant="outline"
className="bg-yellow-100 border-yellow-300 text-yellow-700 hover:bg-yellow-200"
>
🚧 Test Update
</Button>
{showUpdate && (
<UpdateNotification autoShow={false} />
)}
</div>
)
}

View File

@@ -0,0 +1,167 @@
'use client'
import { useState, useEffect } from 'react'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
import { Gift, ShoppingCart, Users, TrendingDown } from 'lucide-react'
import { useSession } from 'next-auth/react'
interface WholesalePricingProps {
productId: string
originalPrice: number
quantity: number
}
interface PricingData {
originalPrice: number
finalPrice: number
discount: number
discountPercentage: number
isWholesalePrice: boolean
}
export default function WholesalePricing({
productId,
originalPrice,
quantity
}: WholesalePricingProps) {
const { data: session } = useSession()
const [pricingData, setPricingData] = useState<PricingData | null>(null)
const [isWholesaler, setIsWholesaler] = useState(false)
const [loading, setLoading] = useState(false)
useEffect(() => {
const fetchPricing = async () => {
if (!session?.user) return
setLoading(true)
try {
const response = await fetch('/api/pricing/wholesale', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
productId,
quantity
}),
})
const data = await response.json()
if (data.success) {
setPricingData(data.pricing)
setIsWholesaler(data.isWholesaler)
}
} catch (error) {
console.error('Failed to fetch pricing:', error)
} finally {
setLoading(false)
}
}
if (session?.user && productId && quantity > 0) {
fetchPricing()
}
}, [session, productId, quantity])
// Don't show if user is not logged in
if (!session?.user) {
return null
}
// Show wholesaler benefits if user is wholesaler
if (isWholesaler) {
return (
<div className="space-y-3">
<Badge className="bg-blue-100 text-blue-800 px-3 py-1">
<Users className="h-4 w-4 mr-1" />
Wholesaler Account
</Badge>
{pricingData && quantity >= 10 && (
<Card className="border-green-200 bg-green-50">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Gift className="h-5 w-5 text-green-600" />
<span className="font-medium text-green-800">
Wholesale Discount Applied!
</span>
</div>
<Badge className="bg-green-100 text-green-800">
25% OFF
</Badge>
</div>
<div className="mt-2 space-y-1">
<div className="flex justify-between text-sm">
<span>Original Price:</span>
<span className="line-through text-gray-500">
{pricingData.originalPrice.toLocaleString()}
</span>
</div>
<div className="flex justify-between text-sm font-medium">
<span>Wholesale Price:</span>
<span className="text-green-600">
{pricingData.finalPrice.toLocaleString()}
</span>
</div>
<div className="flex justify-between text-sm">
<span>You Save:</span>
<span className="text-green-600">
{pricingData.discount.toLocaleString()}
</span>
</div>
</div>
</CardContent>
</Card>
)}
{quantity < 10 && (
<Card className="border-yellow-200 bg-yellow-50">
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<ShoppingCart className="h-5 w-5 text-yellow-600" />
<span className="font-medium text-yellow-800">
Add {10 - quantity} more items for 25% wholesale discount
</span>
</div>
<p className="text-sm text-yellow-700 mt-1">
Minimum 10 items required for bulk pricing
</p>
</CardContent>
</Card>
)}
</div>
)
}
// Show wholesaler invitation if user is not a wholesaler
return (
<Card className="border-blue-200 bg-blue-50">
<CardContent className="p-4">
<div className="flex items-center space-x-2 mb-2">
<TrendingDown className="h-5 w-5 text-blue-600" />
<span className="font-medium text-blue-800">
Want Wholesale Prices?
</span>
</div>
<p className="text-sm text-blue-700 mb-3">
Join our wholesaler program and get 25% off on bulk orders!
</p>
<div className="space-y-1 text-xs text-blue-600">
<div> 25% discount on orders of 10+ items</div>
<div> Dedicated account manager</div>
<div> Priority support</div>
<div> Flexible payment terms</div>
</div>
<a
href="/wholesaler"
className="inline-block mt-3 text-sm font-medium text-blue-600 hover:text-blue-800"
>
Register as Wholesaler
</a>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,88 @@
'use client'
import Link from 'next/link'
import { signOut } from 'next-auth/react'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Bell, Settings, LogOut, Eye } from 'lucide-react'
interface AdminHeaderProps {
user: {
name?: string | null
email?: string | null
image?: string | null
}
}
export function AdminHeader({ user }: AdminHeaderProps) {
return (
<header className="bg-white border-b border-gray-200">
<div className="flex h-16 items-center justify-between px-6">
<div className="flex items-center space-x-4">
<Link href="/admin" className="flex items-center space-x-2">
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-lg">M</span>
</div>
<span className="text-xl font-bold text-gray-900">Padmaaja Rasooi Admin</span>
</Link>
</div>
<div className="flex items-center space-x-4">
<Button variant="ghost" size="sm" asChild>
<Link href="/" className="flex items-center">
<Eye className="h-4 w-4 mr-2" />
View Site
</Link>
</Button>
<Button variant="ghost" size="sm">
<Bell className="h-5 w-5" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Avatar className="h-8 w-8">
<AvatarImage src={user.image || ''} alt={user.name || ''} />
<AvatarFallback>
{user.name?.[0] || user.email?.[0] || 'A'}
</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{user.name}</p>
<p className="text-xs leading-none text-muted-foreground">
{user.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/admin/settings" className="flex items-center">
<Settings className="mr-2 h-4 w-4" />
Settings
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => signOut()}>
<LogOut className="mr-2 h-4 w-4" />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,90 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
LayoutDashboard,
Users,
Package,
ShoppingCart,
DollarSign,
Settings,
BarChart3,
CreditCard,
Tag,
ChevronLeft,
ChevronRight,
Mail,
Star
} from 'lucide-react'
const navigation = [
{ name: 'Dashboard', href: '/admin', icon: LayoutDashboard },
{ name: 'Users', href: '/admin/users', icon: Users },
{ name: 'Products', href: '/admin/products', icon: Package },
{ name: 'Categories', href: '/admin/categories', icon: Tag },
{ name: 'Orders', href: '/admin/orders', icon: ShoppingCart },
{ name: 'Reviews', href: '/admin/reviews', icon: Star },
{ name: 'Form Responses', href: '/admin/forms', icon: Mail },
{ name: 'Commissions', href: '/admin/commissions', icon: DollarSign },
{ name: 'Payouts', href: '/admin/payouts', icon: CreditCard },
{ name: 'Analytics', href: '/admin/analytics', icon: BarChart3 },
{ name: 'Settings', href: '/admin/settings', icon: Settings },
]
export function AdminSidebar() {
const pathname = usePathname()
const [collapsed, setCollapsed] = useState(false)
return (
<div className={cn(
"bg-white border-r border-gray-200 transition-all duration-300",
collapsed ? "w-16" : "w-64"
)}>
<div className="flex h-full flex-col">
<div className="flex items-center justify-between p-4 border-b">
{!collapsed && (
<h2 className="text-lg font-semibold text-gray-900">Admin Panel</h2>
)}
<Button
variant="ghost"
size="sm"
onClick={() => setCollapsed(!collapsed)}
className="h-8 w-8 p-0"
>
{collapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</Button>
</div>
<nav className="flex-1 space-y-1 p-4">
{navigation.map((item) => {
const isActive = pathname === item.href
return (
<Link
key={item.name}
href={item.href}
className={cn(
"flex items-center rounded-lg px-3 py-2 text-sm font-medium transition-colors",
isActive
? "bg-blue-50 text-blue-700"
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
)}
title={collapsed ? item.name : undefined}
>
<item.icon className={cn("h-5 w-5", collapsed ? "mx-auto" : "mr-3")} />
{!collapsed && <span>{item.name}</span>}
</Link>
)
})}
</nav>
</div>
</div>
)
}

View File

@@ -0,0 +1,243 @@
'use client'
import { useState } 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 { Switch } from '@/components/ui/switch'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Save, Upload, X, Plus, Edit } from 'lucide-react'
import { toast } from 'sonner'
import Image from 'next/image'
interface Category {
id?: string
name: string
description: string
image: string | null
isActive: boolean
}
interface CategoryFormDialogProps {
category?: Category
onSuccess?: () => void
trigger?: React.ReactNode
mode?: 'create' | 'edit'
}
export function CategoryFormDialog({
category,
onSuccess,
trigger,
mode = category ? 'edit' : 'create'
}: CategoryFormDialogProps) {
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [formData, setFormData] = useState<Category>({
name: '',
description: '',
image: null,
isActive: true,
...category
})
const handleInputChange = (field: keyof Category, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
// Mock image upload - replace with actual upload logic
const imageUrl = URL.createObjectURL(file)
setFormData(prev => ({ ...prev, image: imageUrl }))
}
const removeImage = () => {
setFormData(prev => ({ ...prev, image: null }))
}
const resetForm = () => {
setFormData({
name: '',
description: '',
image: null,
isActive: true,
...category
})
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
try {
const url = mode === 'edit' && category?.id
? `/api/admin/categories/${category.id}`
: '/api/admin/categories'
const method = mode === 'edit' ? 'PATCH' : 'POST'
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Failed to save category')
}
toast.success(`Category ${mode === 'edit' ? 'updated' : 'created'} successfully`)
setOpen(false)
resetForm()
onSuccess?.()
} catch (error: any) {
toast.error(error.message || `Failed to ${mode === 'edit' ? 'update' : 'create'} category`)
} finally {
setLoading(false)
}
}
const defaultTrigger = mode === 'edit' ? (
<Button variant="ghost" size="sm">
<Edit className="h-4 w-4 mr-2" />
Edit
</Button>
) : (
<Button>
<Plus className="h-4 w-4 mr-2" />
Add Category
</Button>
)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{trigger || defaultTrigger}
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{mode === 'edit' ? 'Edit Category' : 'Create New Category'}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Category Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="name">Category Name</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
rows={4}
placeholder="Describe this category..."
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="isActive">Active Status</Label>
<Switch
id="isActive"
checked={formData.isActive}
onCheckedChange={(checked) => handleInputChange('isActive', checked)}
/>
</div>
<p className="text-sm text-gray-500">
{formData.isActive ? 'Category is visible to customers' : 'Category is hidden from customers'}
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Category Image</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Label htmlFor="image" className="cursor-pointer">
<div className="flex items-center space-x-2 px-4 py-2 border border-dashed border-gray-300 rounded-lg hover:border-gray-400">
<Upload className="h-4 w-4" />
<span>Upload Image</span>
</div>
</Label>
<Input
id="image"
type="file"
accept="image/*"
onChange={handleImageUpload}
className="hidden"
/>
</div>
{formData.image && (
<div className="relative group">
<Image
src={formData.image}
alt="Category image"
width={200}
height={200}
className="rounded-lg object-cover w-full h-48"
/>
<Button
type="button"
variant="destructive"
size="sm"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={removeImage}
>
<X className="h-3 w-3" />
</Button>
</div>
)}
</div>
</CardContent>
</Card>
</div>
<div className="flex justify-end space-x-3">
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
disabled={loading}
>
Cancel
</Button>
<Button type="submit" disabled={loading}>
<Save className="h-4 w-4 mr-2" />
{loading ? 'Saving...' : mode === 'edit' ? 'Update Category' : 'Create Category'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,537 @@
'use client'
import { useState, useCallback, useEffect } from 'react'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
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 {
Search,
Upload,
Image as ImageIcon,
Video,
Music,
File,
FolderPlus,
Loader2,
Check,
X,
Eye
} from 'lucide-react'
import { useDropzone } from 'react-dropzone'
import { toast } from 'sonner'
import Image from 'next/image'
// Helper function to determine content type from URL
const getContentTypeFromUrl = (url: string): string | null => {
// Try to extract extension from the pathname, looking for common image extensions
// even if they're not at the very end (due to timestamps or other suffixes)
const extensionMatch = url.match(/\.(jpg|jpeg|png|gif|webp|svg|mp4|webm|mp3|wav|pdf|doc|docx|txt)(?:-\d+)?(?:\?.*)?$/i)
const extension = extensionMatch ? extensionMatch[1].toLowerCase() : null
if (!extension) {
// Fallback: check if the URL contains any image extension
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']
for (const ext of imageExtensions) {
if (url.toLowerCase().includes(`.${ext}`)) {
return getExtensionMimeType(ext)
}
}
return null
}
return getExtensionMimeType(extension)
}
// Helper function to map extensions to MIME types
const getExtensionMimeType = (extension: string): string | null => {
const extensionMap: Record<string, string> = {
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'webp': 'image/webp',
'svg': 'image/svg+xml',
'mp4': 'video/mp4',
'webm': 'video/webm',
'mp3': 'audio/mpeg',
'wav': 'audio/wav',
'pdf': 'application/pdf',
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'txt': 'text/plain'
}
return extensionMap[extension] || null
}
export interface MediaFile {
id: string
name: string
type: 'file' | 'folder'
size?: number
mimeType?: string
url?: string
path: string
createdAt: string
uploadedBy: {
id: string
name: string
}
}
interface MediaSelectorProps {
onSelect: (file: MediaFile) => void
selectedUrl?: string
allowedTypes?: string[] // e.g., ['image/*', 'video/*']
children: React.ReactNode
}
export default function MediaSelector({
onSelect,
selectedUrl,
allowedTypes = ['image/*', 'video/*', 'audio/*'],
children
}: MediaSelectorProps) {
console.log('MediaSelector component rendered')
const [isOpen, setIsOpen] = useState(false)
const [files, setFiles] = useState<MediaFile[]>([])
const [currentPath, setCurrentPath] = useState('/')
const [searchTerm, setSearchTerm] = useState('')
const [loading, setLoading] = useState(false)
const [uploading, setUploading] = useState(false)
const [selectedFile, setSelectedFile] = useState<MediaFile | null>(null)
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
const fetchFiles = useCallback(async () => {
console.log('fetchFiles called, isOpen:', isOpen, 'currentPath:', currentPath)
if (!isOpen) {
console.log('Dialog not open, skipping fetch')
return
}
setLoading(true)
try {
// Use our blob storage API to list files
const searchParams = new URLSearchParams()
// Use 'media' prefix for all requests for consistency with upload folder structure
const prefix = 'media'
console.log('Fetching files with prefix:', prefix)
searchParams.set('prefix', prefix)
searchParams.set('limit', '100')
const apiUrl = `/api/upload/files?${searchParams.toString()}`
console.log('Making API request to:', apiUrl)
const response = await fetch(apiUrl)
console.log('API response status:', response.status)
if (response.ok) {
const data = await response.json()
console.log('MediaSelector API response:', data) // Debug log
if (data.success) {
// Transform blob storage response to MediaFile format
const blobs = data.data?.blobs || []
console.log('Blobs found:', blobs) // Debug log
const transformedFiles: MediaFile[] = blobs.map((blob: any) => {
const mimeType = getContentTypeFromUrl(blob.url) || 'application/octet-stream'
console.log('Transforming blob:', {
url: blob.url,
pathname: blob.pathname,
detectedMimeType: mimeType
})
return {
id: blob.pathname,
name: blob.pathname.split('/').pop() || blob.pathname,
type: 'file' as const,
size: blob.size,
mimeType,
url: blob.url,
path: blob.pathname,
createdAt: blob.uploadedAt,
uploadedBy: {
id: 'system',
name: 'System'
}
}
})
console.log('Transformed files:', transformedFiles)
console.log('Allowed types:', allowedTypes)
// Filter files based on allowed types
const filteredFiles = transformedFiles.filter((file: MediaFile) => {
if (!file.mimeType) {
console.log('File rejected - no mimeType:', file.name)
return false
}
const isAllowed = allowedTypes.some(type => {
if (type.endsWith('/*')) {
const matches = file.mimeType!.startsWith(type.replace('/*', '/'))
console.log(`Checking ${file.mimeType} against ${type}: ${matches}`)
return matches
}
const matches = file.mimeType === type
console.log(`Checking ${file.mimeType} against ${type}: ${matches}`)
return matches
})
console.log(`File ${file.name} allowed: ${isAllowed}`)
return isAllowed
})
console.log('Filtered files:', filteredFiles)
setFiles(filteredFiles)
} else {
console.error('API request failed:', data)
setFiles([])
}
} else {
console.error('Failed to fetch files, status:', response.status)
setFiles([])
}
} catch (error) {
console.error('Error fetching files:', error)
toast.error('Failed to load files')
} finally {
setLoading(false)
}
}, [currentPath, isOpen, allowedTypes])
useEffect(() => {
console.log('MediaSelector mounted, isOpen:', isOpen)
fetchFiles()
}, [fetchFiles, isOpen])
const onDrop = useCallback(async (acceptedFiles: File[]) => {
setUploading(true)
const formData = new FormData()
acceptedFiles.forEach((file) => {
formData.append('files', file)
})
// Set upload type and folder based on current path
formData.append('type', 'general')
if (currentPath !== '/') {
formData.append('folder', currentPath.substring(1)) // Remove leading slash
} else {
formData.append('folder', 'media')
}
try {
const response = await fetch('/api/upload/files', {
method: 'POST',
body: formData
})
if (response.ok) {
const data = await response.json()
if (data.success) {
toast.success(`Uploaded ${data.data.length} file(s) successfully`)
fetchFiles()
} else {
console.error('Upload failed with data:', data)
throw new Error(data.message || 'Upload failed')
}
} else {
const errorData = await response.text()
console.error('Upload failed with status:', response.status, 'Response:', errorData)
throw new Error(`Upload failed with status ${response.status}: ${errorData}`)
}
} catch (error) {
console.error('Upload error:', error)
toast.error(`Failed to upload files: ${error instanceof Error ? error.message : 'Unknown error'}`)
} finally {
setUploading(false)
}
}, [currentPath, fetchFiles])
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple: true,
accept: allowedTypes.reduce((acc, type) => {
acc[type] = []
return acc
}, {} as Record<string, string[]>)
})
const handleFileSelect = (file: MediaFile) => {
if (file.type === 'folder') {
setCurrentPath(currentPath === '/' ? `/${file.name}` : `${currentPath}/${file.name}`)
setSelectedFile(null)
setPreviewUrl(null)
} else {
setSelectedFile(file)
setPreviewUrl(file.url || null)
}
}
const handleConfirmSelection = () => {
if (selectedFile) {
onSelect(selectedFile)
setIsOpen(false)
setSelectedFile(null)
setPreviewUrl(null)
}
}
const navigateUp = () => {
const pathParts = currentPath.split('/').filter(p => p)
if (pathParts.length > 0) {
pathParts.pop()
setCurrentPath(pathParts.length === 0 ? '/' : '/' + pathParts.join('/'))
}
}
const getFileIcon = (file: MediaFile) => {
if (file.type === 'folder') return <FolderPlus className="h-8 w-8 text-blue-500" />
if (!file.mimeType) return <File className="h-8 w-8 text-gray-500" />
if (file.mimeType.startsWith('image/')) return <ImageIcon className="h-8 w-8 text-green-500" />
if (file.mimeType.startsWith('video/')) return <Video className="h-8 w-8 text-red-500" />
if (file.mimeType.startsWith('audio/')) return <Music className="h-8 w-8 text-purple-500" />
return <File className="h-8 w-8 text-gray-500" />
}
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const filteredFiles = files.filter(file =>
file.name.toLowerCase().includes(searchTerm.toLowerCase())
)
return (
<Dialog open={isOpen} onOpenChange={(open) => {
console.log('Dialog onOpenChange called with:', open)
setIsOpen(open)
}}>
<DialogTrigger asChild>
{children}
</DialogTrigger>
<DialogContent className="max-w-7xl max-h-[90vh] w-[95vw] p-0">
<DialogHeader className="p-4 sm:p-6 pb-2 sm:pb-4">
<DialogTitle className="text-lg sm:text-xl">Select Media</DialogTitle>
</DialogHeader>
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-4 p-4 sm:p-6 pt-0 h-[80vh] lg:h-[600px]">
{/* File Browser */}
<div className="lg:col-span-2 lg:border-r lg:pr-4 flex-1 lg:flex-none">
<Tabs defaultValue="browse" className="h-full flex flex-col">
<TabsList className="grid w-full grid-cols-2 mb-4">
<TabsTrigger value="browse" className="text-sm">Browse Files</TabsTrigger>
<TabsTrigger value="upload" className="text-sm">Upload New</TabsTrigger>
</TabsList>
<TabsContent value="browse" className="flex-1 flex flex-col space-y-3 sm:space-y-4">
{/* Navigation */}
<div className="flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0 sm:space-x-2">
{currentPath !== '/' && (
<Button variant="outline" size="sm" onClick={navigateUp} className="w-fit">
Back
</Button>
)}
<div className="text-xs sm:text-sm text-gray-600 break-all">
Path: {currentPath}
</div>
</div>
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="Search files..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 text-sm"
/>
</div>
{/* Files Grid */}
<div className="flex-1 overflow-y-auto h-auto max-h-[400px]">
{loading ? (
<div className="flex items-center justify-center h-32">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-3 xl:grid-cols-4 gap-2 sm:gap-3">
{filteredFiles.map((file) => (
<div
key={file.id}
className={`relative p-2 sm:p-3 border rounded-lg cursor-pointer transition-all hover:shadow-md touch-manipulation ${
selectedFile?.id === file.id
? 'border-blue-500 bg-blue-50'
: 'border-gray-200'
}`}
onClick={() => handleFileSelect(file)}
>
<div className="aspect-square bg-gray-100 rounded-md flex items-center justify-center mb-2 overflow-hidden">
{file.mimeType?.startsWith('image/') && file.url ? (
<Image
src={file.url}
alt={file.name}
width={80}
height={80}
className="object-cover w-full h-full"
/>
) : (
<div className="flex items-center justify-center w-full h-full">
{getFileIcon(file)}
</div>
)}
</div>
<div className="text-xs">
<p className="font-medium truncate" title={file.name}>
{file.name}
</p>
{file.size && (
<p className="text-gray-500 text-xs">
{formatFileSize(file.size)}
</p>
)}
</div>
{selectedFile?.id === file.id && (
<div className="absolute top-1 right-1 sm:top-2 sm:right-2">
<div className="bg-blue-500 text-white rounded-full p-1">
<Check className="h-3 w-3" />
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
</TabsContent>
<TabsContent value="upload" className="flex-1">
<div
{...getRootProps()}
className={`h-full min-h-[200px] border-2 border-dashed rounded-lg flex flex-col items-center justify-center transition-colors p-4 ${
isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
}`}
>
<input {...getInputProps()} />
{uploading ? (
<div className="text-center">
<Loader2 className="h-8 w-8 sm:h-12 sm:w-12 animate-spin text-blue-500 mx-auto mb-4" />
<p className="text-blue-600 text-sm sm:text-base">Uploading files...</p>
</div>
) : (
<div className="text-center">
<Upload className="h-8 w-8 sm:h-12 sm:w-12 text-gray-400 mx-auto mb-4" />
<p className="text-sm sm:text-lg font-medium text-gray-900 mb-2">
Drop files here or click to browse
</p>
<p className="text-xs sm:text-sm text-gray-500">
Supported: {allowedTypes.join(', ')}
</p>
</div>
)}
</div>
</TabsContent>
</Tabs>
</div>
{/* Preview Panel */}
<div className="flex flex-col min-h-[300px] lg:min-h-0">
<h3 className="font-semibold mb-4 text-sm sm:text-base">Preview</h3>
{selectedFile && previewUrl ? (
<div className="flex-1 flex flex-col space-y-3 sm:space-y-4">
<div className="aspect-4/3 bg-gray-100 rounded-lg overflow-hidden flex items-center justify-center">
{selectedFile.mimeType?.startsWith('image/') ? (
<Image
src={previewUrl}
alt={selectedFile.name}
width={200}
height={200}
className="object-contain w-full h-full"
/>
) : selectedFile.mimeType?.startsWith('video/') ? (
<video controls className="w-full h-full max-h-full">
<source src={previewUrl} type={selectedFile.mimeType} />
</video>
) : selectedFile.mimeType?.startsWith('audio/') ? (
<div className="w-full p-4">
<div className="flex items-center justify-center mb-4">
<Music className="h-12 w-12 sm:h-16 sm:w-16 text-purple-500" />
</div>
<audio controls className="w-full">
<source src={previewUrl} type={selectedFile.mimeType} />
</audio>
</div>
) : (
<div className="text-center p-4">
{getFileIcon(selectedFile)}
<p className="text-xs sm:text-sm text-gray-500 mt-2">No preview available</p>
</div>
)}
</div>
<div className="space-y-2">
<p className="font-medium text-sm break-all" title={selectedFile.name}>
{selectedFile.name}
</p>
{selectedFile.size && (
<p className="text-xs text-gray-500">
{formatFileSize(selectedFile.size)}
</p>
)}
<Badge variant="outline" className="text-xs">
{selectedFile.mimeType}
</Badge>
</div>
<div className="space-y-2 mt-auto flex items-center justify-between gap-2">
<Button
onClick={handleConfirmSelection}
className="w-full text-sm"
disabled={!selectedFile}
>
<Check className="h-4 w-4 mr-2" />
Select This File
</Button>
{selectedUrl && (
<Button
variant="outline"
onClick={() => {
onSelect({ url: '', name: '', id: '', type: 'file', path: '', createdAt: '', uploadedBy: { id: '', name: '' } })
setIsOpen(false)
}}
className="w-full text-sm !m-0"
>
<X className="h-4 w-4 mr-2" />
Remove Current
</Button>
)}
</div>
</div>
) : (
<div className="flex-1 flex items-center justify-center text-gray-500">
<div className="text-center">
<Eye className="h-8 w-8 sm:h-12 sm:w-12 mx-auto mb-2 opacity-50" />
<p className="text-sm sm:text-base">Select a file to preview</p>
</div>
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,419 @@
'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
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 { Switch } from '@/components/ui/switch'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { ArrowLeft, Save, Upload, X, ImageIcon } from 'lucide-react'
import { toast } from 'sonner'
import Link from 'next/link'
import Image from 'next/image'
import MediaSelector, { MediaFile } from '@/components/admin/MediaSelector'
interface Product {
id?: string
name: string
description: string
price: number
discount: number
images: string[]
stock: number
manageStock: boolean
sku: string
isActive: boolean
categoryId: string
}
interface Category {
id: string
name: string
}
interface ProductFormProps {
product?: Product
}
export function ProductForm({ product }: ProductFormProps) {
const router = useRouter()
const [loading, setLoading] = useState(false)
const [categoriesLoading, setCategoriesLoading] = useState(true)
const [categories, setCategories] = useState<Category[]>([])
const [imageUrl, setImageUrl] = useState('')
const [formData, setFormData] = useState<Product>({
name: '',
description: '',
price: 0,
discount: 0,
images: [],
stock: 0,
manageStock: true,
sku: '',
isActive: true,
categoryId: '',
...product
})
useEffect(() => {
fetchCategories()
}, [])
const fetchCategories = async () => {
try {
setCategoriesLoading(true)
const response = await fetch('/api/categories')
const data = await response.json()
// Handle the new API response structure
setCategories(data.categories || [])
} catch (error) {
console.error('Error fetching categories:', error)
setCategories([]) // Fallback to empty array
} finally {
setCategoriesLoading(false)
}
}
const handleInputChange = (field: keyof Product, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
const handleAddImageUrl = () => {
if (!imageUrl.trim()) {
toast.error('Please enter a valid image URL')
return
}
// Basic URL validation
try {
new URL(imageUrl)
} catch {
toast.error('Please enter a valid URL')
return
}
setFormData(prev => ({
...prev,
images: [...prev.images, imageUrl.trim()]
}))
setImageUrl('')
toast.success('Image URL added')
}
const removeImage = (index: number) => {
setFormData(prev => ({
...prev,
images: prev.images.filter((_, i) => i !== index)
}))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
try {
const url = product
? `/api/admin/products/${product.id}`
: '/api/admin/products'
const method = product ? '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 product')
toast.success(`Product ${product ? 'updated' : 'created'} successfully`)
router.push('/admin/products')
} catch (error) {
toast.error(`Failed to ${product ? 'update' : 'create'} product`)
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="flex items-center justify-between">
<Button variant="ghost" asChild>
<Link href="/admin/products">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Products
</Link>
</Button>
<Button type="submit" disabled={loading}>
<Save className="h-4 w-4 mr-2" />
{loading ? 'Saving...' : 'Save Product'}
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<Card>
<CardHeader>
<CardTitle>Product Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="name">Product Name</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
rows={4}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="sku">SKU</Label>
<Input
id="sku"
value={formData.sku}
onChange={(e) => handleInputChange('sku', e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="category">Category</Label>
<Select
value={formData.categoryId}
onValueChange={(value) => handleInputChange('categoryId', value)}
disabled={categoriesLoading}
>
<SelectTrigger>
<SelectValue placeholder={
categoriesLoading
? "Loading categories..."
: categories.length === 0
? "No categories available"
: "Select category"
} />
</SelectTrigger>
<SelectContent>
{!categoriesLoading && categories.length > 0 ? (
categories.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name}
</SelectItem>
))
) : null}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Product Images</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Media Selector Section */}
<div className="space-y-2">
<Label className="text-sm font-medium">Product Images</Label>
<MediaSelector
allowedTypes={['image/*']}
onSelect={(file: MediaFile) => {
const imageUrl = file.url
if (imageUrl && typeof imageUrl === 'string' && imageUrl.trim()) {
setFormData(prev => ({
...prev,
images: [...prev.images, imageUrl]
}))
toast.success('Image added successfully')
} else {
toast.error('Invalid file selected - no valid URL found')
}
}}
>
<div className="flex items-center space-x-2 px-4 py-2 border border-dashed border-gray-300 rounded-lg hover:border-gray-400 cursor-pointer">
<ImageIcon className="h-4 w-4" />
<span>Browse & Upload Images</span>
</div>
</MediaSelector>
<p className="text-xs text-gray-500">
Browse existing images or upload new ones. Supports JPG, PNG, WebP formats.
</p>
</div>
{/* URL Input Section (Optional) */}
<div className="space-y-2">
<Label className="text-sm font-medium">Add Image URL (Optional)</Label>
<div className="flex space-x-2">
<Input
placeholder="https://example.com/image.jpg"
value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAddImageUrl()}
/>
<Button
type="button"
variant="outline"
onClick={handleAddImageUrl}
disabled={!imageUrl.trim()}
>
Add URL
</Button>
</div>
<p className="text-xs text-gray-500">
Add images from external URLs (e.g., Unsplash, your CDN, etc.)
</p>
</div>
{/* Image Preview Grid */}
{formData.images.length > 0 && (
<div className="space-y-2">
<Label className="text-sm font-medium">Image Preview</Label>
<div className="grid grid-cols-3 gap-4">
{formData.images.map((image, index) => (
<div key={index} className="relative group">
<Image
src={image}
alt={`Product image ${index + 1}`}
width={150}
height={150}
className="rounded-lg object-cover w-full h-32 border"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = '/placeholder-image.jpg'; // fallback image
}}
/>
<Button
type="button"
variant="destructive"
size="sm"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity h-6 w-6 p-0"
onClick={() => removeImage(index)}
>
<X className="h-3 w-3" />
</Button>
<div className="absolute bottom-0 left-0 right-0 bg-black/50 text-white text-xs p-1 rounded-b-lg truncate">
{image.startsWith('/products/') ? 'Uploaded' : 'External URL'}
</div>
</div>
))}
</div>
</div>
)}
{formData.images.length === 0 && (
<div className="text-center py-8 border-2 border-dashed border-gray-200 rounded-lg">
<Upload className="h-8 w-8 mx-auto text-gray-400 mb-2" />
<p className="text-sm text-gray-500">No images added yet</p>
<p className="text-xs text-gray-400">Upload files or add URLs to get started</p>
</div>
)}
</div>
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Pricing & Inventory</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="price">Price ()</Label>
<Input
id="price"
type="number"
step="0.01"
value={formData.price}
onChange={(e) => handleInputChange('price', parseFloat(e.target.value) || 0)}
required
/>
</div>
<div>
<Label htmlFor="discount">Discount (%)</Label>
<Input
id="discount"
type="number"
min="0"
max="100"
value={formData.discount}
onChange={(e) => handleInputChange('discount', parseInt(e.target.value) || 0)}
/>
</div>
<div>
<div className="flex items-center justify-between mb-3">
<Label htmlFor="manageStock">Manage Stock</Label>
<Switch
id="manageStock"
checked={formData.manageStock}
onCheckedChange={(checked) => handleInputChange('manageStock', checked)}
/>
</div>
<p className="text-sm text-gray-500 mb-4">
Enable this to track inventory levels for this product
</p>
{formData.manageStock && (
<div>
<Label htmlFor="stock">Stock Quantity</Label>
<Input
id="stock"
type="number"
min="0"
value={formData.stock}
onChange={(e) => handleInputChange('stock', parseInt(e.target.value) || 0)}
required
/>
</div>
)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Status</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<Label htmlFor="isActive">Active Status</Label>
<Switch
id="isActive"
checked={formData.isActive}
onCheckedChange={(checked) => handleInputChange('isActive', checked)}
/>
</div>
<p className="text-sm text-gray-500 mt-2">
{formData.isActive ? 'Product is visible to customers' : 'Product is hidden from customers'}
</p>
</CardContent>
</Card>
</div>
</div>
</form>
)
}

View File

@@ -0,0 +1,66 @@
'use client'
import { Button } from '@/components/ui/button'
import { ArrowLeft } from 'lucide-react'
import { motion } from 'framer-motion'
import Link from 'next/link'
import { ReactNode } from 'react'
interface DashboardHeaderProps {
title: string
description?: string
icon?: ReactNode
backHref?: string
actions?: ReactNode
}
export function DashboardHeader({
title,
description,
icon,
backHref = "/dashboard",
actions
}: DashboardHeaderProps) {
return (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl shadow-lg border border-gray-200 p-8"
>
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6">
<div className="flex items-center space-x-4">
<Button
variant="ghost"
asChild
className="w-10 h-10 rounded-full bg-gray-100 p-0 hover:bg-gray-200"
>
<Link href={backHref}>
<ArrowLeft className="h-4 w-4" />
</Link>
</Button>
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl flex items-center justify-center">
{icon}
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900">
{title}
</h1>
{description && (
<p className="text-gray-600 text-sm">{description}</p>
)}
</div>
</div>
</div>
{actions && (
<div className="flex items-center space-x-3">
{actions}
</div>
)}
</div>
</motion.div>
)
}
export { DashboardHeader as default }

View File

@@ -0,0 +1,477 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { usePathname } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { cn } from '@/lib/utils'
import {
LayoutDashboard,
Users,
Network,
Wallet,
ShoppingBag,
BarChart3,
CreditCard,
User,
Settings,
LogOut,
ChevronLeft,
ChevronRight,
Trophy,
Gift,
TrendingUp,
Package
} from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
import { useSession, signOut } from 'next-auth/react'
interface SidebarItem {
title: string
href: string
icon: React.ComponentType<{ className?: string }>
badge?: string
children?: SidebarItem[]
roles?: string[] // Allowed roles for this item
}
// Role-based navigation items
const getSidebarItems = (userRole: string): SidebarItem[] => {
const allItems: SidebarItem[] = [
{
title: 'Dashboard',
href: '/dashboard',
icon: LayoutDashboard,
roles: ['CUSTOMER', 'MEMBER', 'WHOLESALER', 'PART_TIME', 'ADMIN']
},
{
title: 'My Orders',
href: '/dashboard/orders',
icon: ShoppingBag,
roles: ['CUSTOMER', 'MEMBER', 'WHOLESALER', 'PART_TIME', 'ADMIN']
},
{
title: 'Network',
href: '',
icon: Network,
roles: ['MEMBER', 'WHOLESALER'],
children: [
{
title: 'Team Genealogy',
href: '/dashboard/genealogy',
icon: Users,
roles: ['MEMBER', 'WHOLESALER']
},
{
title: 'My Referrals',
href: '/dashboard/referrals',
icon: Gift,
roles: ['MEMBER', 'WHOLESALER']
},
{
title: 'Team Overview',
href: '/dashboard/team',
icon: TrendingUp,
roles: ['MEMBER', 'WHOLESALER']
},
]
},
{
title: 'Achievements',
href: '/dashboard/achievements',
icon: Trophy,
roles: ['MEMBER', 'WHOLESALER']
},
{
title: 'Wallet & Earnings',
href: '/dashboard/wallet',
icon: Wallet,
roles: ['MEMBER', 'WHOLESALER', 'PART_TIME'],
children: [
{
title: 'My Wallet',
href: '/dashboard/wallet',
icon: Wallet,
roles: ['MEMBER', 'WHOLESALER', 'PART_TIME']
},
{
title: 'Commission History',
href: '/dashboard/commissions',
icon: TrendingUp,
roles: ['MEMBER', 'WHOLESALER']
},
{
title: 'Earnings',
href: '/dashboard/earnings',
icon: TrendingUp,
roles: ['PART_TIME']
},
{
title: 'Payout Requests',
href: '/dashboard/payouts',
icon: CreditCard,
roles: ['MEMBER', 'WHOLESALER', 'PART_TIME']
},
]
},
{
title: 'Wholesale Portal',
href: '/dashboard/wholesale',
icon: Package,
roles: ['WHOLESALER']
},
{
title: 'Part-Time Jobs',
href: '/dashboard/part-time',
icon: Users,
roles: ['PART_TIME']
},
{
title: 'Reports & Analytics',
href: '/dashboard/reports',
icon: BarChart3,
roles: ['MEMBER', 'ADMIN']
},
{
title: 'Profile Settings',
href: '/dashboard/profile',
icon: User,
roles: ['CUSTOMER', 'MEMBER', 'WHOLESALER', 'PART_TIME', 'ADMIN']
},
]
// Filter items based on user role
const filterItemsByRole = (items: SidebarItem[]): SidebarItem[] => {
return items
.filter(item => !item.roles || item.roles.includes(userRole))
.map(item => ({
...item,
children: item.children ? filterItemsByRole(item.children) : undefined
}))
.filter(item => !item.children || item.children.length > 0) // Remove parent items with no visible children
}
return filterItemsByRole(allItems)
}
// Role display information
const getRoleInfo = (role: string) => {
const roleMap: Record<string, { label: string; description: string; className: string }> = {
ADMIN: {
label: 'Admin',
description: 'Full system access',
className: 'bg-purple-100 text-purple-800 border-purple-200'
},
MEMBER: {
label: 'Partner',
description: 'Marketing partner',
className: 'bg-blue-100 text-blue-800 border-blue-200'
},
WHOLESALER: {
label: 'Wholesaler',
description: 'Bulk distribution partner',
className: 'bg-yellow-100 text-yellow-800 border-yellow-200'
},
PART_TIME: {
label: 'Part-time',
description: 'Delivery & support team',
className: 'bg-green-100 text-green-800 border-green-200'
},
CUSTOMER: {
label: 'Customer',
description: 'Valued customer',
className: 'bg-gray-100 text-gray-800 border-gray-200'
}
}
return roleMap[role] || roleMap.CUSTOMER
}
interface DashboardSidebarProps {
className?: string
}
export function DashboardSidebar({ className }: DashboardSidebarProps) {
const [isCollapsed, setIsCollapsed] = useState(false)
const [expandedItems, setExpandedItems] = useState<string[]>([])
const pathname = usePathname()
const { data: session } = useSession()
// Get user role from session, default to CUSTOMER
const userRole = session?.user?.role || 'CUSTOMER'
const roleInfo = getRoleInfo(userRole)
// Get sidebar items based on user role
const sidebarItems = getSidebarItems(userRole)
const toggleExpanded = (title: string) => {
setExpandedItems(prev =>
prev.includes(title)
? prev.filter(item => item !== title)
: [...prev, title]
)
}
const isActive = (href: string) => {
if (href === '/dashboard') {
return pathname === '/dashboard'
}
return pathname.startsWith(href)
}
const hasActiveChild = (children?: SidebarItem[]) => {
if (!children) return false
return children.some(child => isActive(child.href))
}
return (
<motion.div
initial={{ x: -100, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
className={cn(
"bg-white/80 backdrop-blur-sm border-r border-gray-200 h-screen sticky top-0 overflow-y-auto transition-all duration-300",
isCollapsed ? "w-16" : "w-64",
className
)}
>
<div className="p-4">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<AnimatePresence>
{!isCollapsed && (
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="flex items-center space-x-3"
>
<div className='w-8 h-8 rounded-lg flex items-center justify-center'>
<Image src="/logo.png" alt="Padmaaja Logo" width={32} height={32} className="w-full h-full object-contain" />
</div>
<span className="font-bold text-gray-900">Padmaaja Rasooi</span>
</motion.div>
)}
</AnimatePresence>
<Button
variant="ghost"
size="sm"
onClick={() => setIsCollapsed(!isCollapsed)}
className="h-8 w-8 p-0"
>
{isCollapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</Button>
</div>
{/* User Profile */}
<AnimatePresence>
{!isCollapsed && session && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg p-4 mb-6"
>
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-r from-blue-500 to-purple-600 rounded-full flex items-center justify-center">
{session.user.image ? (
<Image src={session.user.image} alt="User Avatar" width={40} height={40} className="rounded-full" />
) : (
<User className="h-5 w-5 text-white" />
)}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-gray-900 truncate">
{session.user.name || 'User'}
</p>
<div className="flex items-center space-x-2 mt-1">
<Badge
variant="outline"
className={cn("text-xs", roleInfo.className)}
>
{roleInfo.label}
</Badge>
</div>
<p className="text-xs text-gray-500 mt-1">
{roleInfo.description}
</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Navigation */}
<nav className="space-y-2">
{sidebarItems.map((item) => (
<div key={item.title}>
<div
className={cn(
"flex items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors cursor-pointer",
isActive(item.href) || hasActiveChild(item.children)
? "bg-blue-100 text-blue-900"
: "text-gray-700 hover:bg-gray-100",
isCollapsed && "justify-center px-2"
)}
onClick={() => {
if (item.children) {
toggleExpanded(item.title)
}
}}
>
<Link href={item.href} className="flex items-center space-x-3 flex-1">
<item.icon className="h-5 w-5" />
<AnimatePresence>
{!isCollapsed && (
<motion.span
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
className="truncate"
>
{item.title}
</motion.span>
)}
</AnimatePresence>
</Link>
<AnimatePresence>
{!isCollapsed && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex items-center space-x-2"
>
{item.badge && (
<Badge variant="secondary" className="text-xs">
{item.badge}
</Badge>
)}
{item.children && (
<ChevronRight
className={cn(
"h-4 w-4 transition-transform",
expandedItems.includes(item.title) && "rotate-90"
)}
/>
)}
</motion.div>
)}
</AnimatePresence>
</div>
{/* Submenu */}
<AnimatePresence>
{item.children && expandedItems.includes(item.title) && !isCollapsed && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="ml-4 mt-2 space-y-1"
>
{item.children.map((child) => (
<Link
key={child.title}
href={child.href}
className={cn(
"flex items-center space-x-3 rounded-lg px-3 py-2 text-sm transition-colors",
isActive(child.href)
? "bg-blue-50 text-blue-700 border-l-2 border-blue-500"
: "text-gray-600 hover:bg-gray-50"
)}
>
<child.icon className="h-4 w-4" />
<span className="truncate">{child.title}</span>
</Link>
))}
</motion.div>
)}
</AnimatePresence>
</div>
))}
</nav>
{/* Footer Actions */}
<div className="mt-8 pt-6 border-t border-gray-200 space-y-2">
<Link
href="/"
target="_blank"
className={cn(
"flex items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors text-gray-700 hover:bg-gray-100",
isCollapsed && "justify-center px-2"
)}
>
<Package className="h-5 w-5" />
<AnimatePresence>
{!isCollapsed && (
<motion.span
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
>
View Site
</motion.span>
)}
</AnimatePresence>
</Link>
{session?.user?.role === 'ADMIN' && (
<Link
href="/admin"
className={cn(
"flex items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
pathname.startsWith('/admin')
? "bg-purple-100 text-purple-900"
: "text-gray-700 hover:bg-gray-100",
isCollapsed && "justify-center px-2"
)}
>
<Settings className="h-5 w-5" />
<AnimatePresence>
{!isCollapsed && (
<motion.span
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
>
Admin Panel
</motion.span>
)}
</AnimatePresence>
</Link>
)}
<Button
variant="ghost"
onClick={() => signOut()}
className={cn(
"w-full justify-start text-red-600 hover:text-red-700 hover:bg-red-50",
isCollapsed && "justify-center px-2"
)}
>
<LogOut className="h-5 w-5" />
<AnimatePresence>
{!isCollapsed && (
<motion.span
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
className="ml-3"
>
Sign Out
</motion.span>
)}
</AnimatePresence>
</Button>
</div>
</div>
</motion.div>
)
}

View File

@@ -0,0 +1,209 @@
'use client'
import { useState } from 'react'
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 { Send } from 'lucide-react'
import { toast } from 'sonner'
interface ContactFormProps {
className?: string
variant?: 'default' | 'minimal' | 'professional'
onSubmitSuccess?: () => void
showInquiryType?: boolean
title?: string
description?: string
}
export default function ContactForm({}: ContactFormProps) {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: '',
inquiry_type: ''
})
const [isSubmitting, setIsSubmitting] = useState(false)
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setFormData(prev => ({
...prev,
[name]: value
}))
}
const handleSelectChange = (value: string) => {
setFormData(prev => ({
...prev,
inquiry_type: value
}))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
try {
const response = await fetch('/api/forms', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
formId: 'contact', // Add formId for the simplified schema
name: formData.name,
email: formData.email,
subject: formData.subject,
message: formData.message,
inquiryType: formData.inquiry_type,
formSource: 'contact-page'
})
})
const result = await response.json()
if (result.success) {
toast.success('Message sent successfully! We\'ll get back to you soon.')
setFormData({
name: '',
email: '',
subject: '',
message: '',
inquiry_type: ''
})
} else {
throw new Error(result.message || 'Failed to send message')
}
} catch (error) {
console.error('Form submission error:', error)
toast.error('Failed to send message. Please try again.')
} finally {
setIsSubmitting(false)
}
}
return (
<div className="p-6 md:p-8">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Name & Email Row */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="name" className="text-sm font-medium text-gray-700 mb-2 block">
Name
</Label>
<Input
id="name"
name="name"
type="text"
placeholder="Your full name"
value={formData.name}
onChange={handleInputChange}
className="h-11 rounded-lg border-gray-200 focus:border-orange-400 focus:ring-2 focus:ring-orange-100 transition-all duration-200"
required
/>
</div>
<div>
<Label htmlFor="email" className="text-sm font-medium text-gray-700 mb-2 block">
Email
</Label>
<Input
id="email"
name="email"
type="email"
placeholder="your@company.com"
value={formData.email}
onChange={handleInputChange}
className="h-11 rounded-lg border-gray-200 focus:border-orange-400 focus:ring-2 focus:ring-orange-100 transition-all duration-200"
required
/>
</div>
</div>
{/* Inquiry Type */}
<div>
<Label htmlFor="inquiry_type" className="text-sm font-medium text-gray-700 mb-2 block">
Inquiry Type
</Label>
<Select value={formData.inquiry_type} onValueChange={handleSelectChange}>
<SelectTrigger className="h-11 rounded-lg border-gray-200 focus:border-orange-400 focus:ring-2 focus:ring-orange-100">
<SelectValue placeholder="Select inquiry type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="wholesale">Wholesale Partnership</SelectItem>
<SelectItem value="bulk">Bulk Orders (1000+ kg)</SelectItem>
<SelectItem value="pricing">Pricing & Catalog</SelectItem>
<SelectItem value="partnership">Supply Chain Partnership</SelectItem>
<SelectItem value="private-label">Private Label</SelectItem>
<SelectItem value="export">Export & Trade</SelectItem>
<SelectItem value="franchise">Franchise</SelectItem>
<SelectItem value="quality">Quality Certificates</SelectItem>
<SelectItem value="custom-packaging">Custom Packaging</SelectItem>
<SelectItem value="general">General Inquiry</SelectItem>
</SelectContent>
</Select>
</div>
{/* Subject */}
<div>
<Label htmlFor="subject" className="text-sm font-medium text-gray-700 mb-2 block">
Subject
</Label>
<Input
id="subject"
name="subject"
type="text"
placeholder="Brief subject"
value={formData.subject}
onChange={handleInputChange}
className="h-11 rounded-lg border-gray-200 focus:border-orange-400 focus:ring-2 focus:ring-orange-100 transition-all duration-200"
required
/>
</div>
{/* Message */}
<div>
<Label htmlFor="message" className="text-sm font-medium text-gray-700 mb-2 block">
Message
</Label>
<Textarea
id="message"
name="message"
placeholder="Tell us about your requirements, quantities, and timeline..."
rows={5}
value={formData.message}
onChange={handleInputChange}
className="rounded-lg border-gray-200 focus:border-orange-400 focus:ring-2 focus:ring-orange-100 resize-none transition-all duration-200"
required
/>
</div>
{/* Submit Button */}
<Button
type="submit"
className="w-full h-12 bg-orange-600 hover:bg-orange-700 text-white font-medium rounded-lg transition-colors duration-200"
disabled={isSubmitting}
>
{isSubmitting ? (
<div className="flex items-center justify-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
<span>Sending...</span>
</div>
) : (
<div className="flex items-center justify-center space-x-2">
<Send className="h-4 w-4" />
<span>Send Message</span>
</div>
)}
</Button>
<p className="text-xs text-gray-500 text-center mt-4">
We'll respond within 24 hours
</p>
</form>
</div>
)
}

View File

@@ -0,0 +1,517 @@
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Clock,
User,
Mail,
Phone,
MapPin,
Briefcase,
FileText,
CheckCircle,
Loader2,
Award,
GraduationCap
} from 'lucide-react'
import { toast } from 'sonner'
interface PartTimeRegistrationFormProps {
isOpen: boolean
onClose: () => void
}
interface PartTimeFormData {
// Personal Information
firstName: string
lastName: string
email: string
phone: string
age: string
gender: string
// Address Information
address: string
city: string
state: string
zipCode: string
// Professional Information
education: string
experience: string
preferredRole: string
availableHours: string
availableDays: string
// Additional Information
skills: string
motivation: string
previousWorkExperience: string
languagesKnown: string
}
export default function PartTimeRegistrationForm({ isOpen, onClose }: PartTimeRegistrationFormProps) {
const [loading, setLoading] = useState(false)
const [showSuccessModal, setShowSuccessModal] = useState(false)
const [loginCredentials, setLoginCredentials] = useState<{
email: string
password: string
} | null>(null)
const [formData, setFormData] = useState<PartTimeFormData>({
firstName: '',
lastName: '',
email: '',
phone: '',
age: '',
gender: '',
address: '',
city: '',
state: '',
zipCode: '',
education: '',
experience: '',
preferredRole: '',
availableHours: '',
availableDays: '',
skills: '',
motivation: '',
previousWorkExperience: '',
languagesKnown: ''
})
const handleInputChange = (field: keyof PartTimeFormData, value: string) => {
setFormData(prev => ({
...prev,
[field]: value
}))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
try {
const response = await fetch('/api/part-time/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || 'Registration failed')
}
setLoginCredentials(data.loginCredentials)
setShowSuccessModal(true)
onClose() // Close the main registration modal
// Reset form
setFormData({
firstName: '',
lastName: '',
email: '',
phone: '',
age: '',
gender: '',
address: '',
city: '',
state: '',
zipCode: '',
education: '',
experience: '',
preferredRole: '',
availableHours: '',
availableDays: '',
skills: '',
motivation: '',
previousWorkExperience: '',
languagesKnown: ''
})
toast.success('Sales Executive application submitted successfully!')
} catch (error) {
console.error('Registration error:', error)
toast.error(error instanceof Error ? error.message : 'Application submission failed')
} finally {
setLoading(false)
}
}
return (
<>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-2xl flex items-center">
<Briefcase className="h-6 w-6 mr-3" />
Sales Executive Application
</DialogTitle>
<DialogDescription>
Apply for our Sales Executive position with guaranteed 10,000 salary + 1% commission
</DialogDescription>
</DialogHeader>
<div className="flex items-center justify-center gap-4 mb-6">
<Badge className="bg-green-100 text-green-800 px-4 py-2">
<Clock className="h-4 w-4 mr-2" />
10,000 Guaranteed
</Badge>
<Badge className="bg-blue-100 text-blue-800 px-4 py-2">
<Award className="h-4 w-4 mr-2" />
1% Sales Commission
</Badge>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Personal Information */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<User className="h-5 w-5 mr-2 text-green-600" />
Personal Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="firstName">First Name *</Label>
<Input
id="firstName"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
required
className="mt-1"
/>
</div>
<div>
<Label htmlFor="lastName">Last Name *</Label>
<Input
id="lastName"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
required
className="mt-1"
/>
</div>
<div>
<Label htmlFor="email">Email Address *</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
required
className="mt-1"
/>
</div>
<div>
<Label htmlFor="phone">Phone Number *</Label>
<Input
id="phone"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
required
className="mt-1"
/>
</div>
<div>
<Label htmlFor="age">Age *</Label>
<Input
id="age"
type="number"
value={formData.age}
onChange={(e) => handleInputChange('age', e.target.value)}
required
className="mt-1"
/>
</div>
<div>
<Label htmlFor="gender">Gender *</Label>
<Select value={formData.gender} onValueChange={(value) => handleInputChange('gender', value)}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select gender" />
</SelectTrigger>
<SelectContent>
<SelectItem value="male">Male</SelectItem>
<SelectItem value="female">Female</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{/* Address Information */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<MapPin className="h-5 w-5 mr-2 text-green-600" />
Address Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2">
<Label htmlFor="address">Full Address *</Label>
<Input
id="address"
value={formData.address}
onChange={(e) => handleInputChange('address', e.target.value)}
required
className="mt-1"
/>
</div>
<div>
<Label htmlFor="city">City *</Label>
<Input
id="city"
value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)}
required
className="mt-1"
/>
</div>
<div>
<Label htmlFor="state">State *</Label>
<Input
id="state"
value={formData.state}
onChange={(e) => handleInputChange('state', e.target.value)}
required
className="mt-1"
/>
</div>
<div>
<Label htmlFor="zipCode">ZIP Code *</Label>
<Input
id="zipCode"
value={formData.zipCode}
onChange={(e) => handleInputChange('zipCode', e.target.value)}
required
className="mt-1"
/>
</div>
</div>
</div>
{/* Professional Information */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<GraduationCap className="h-5 w-5 mr-2 text-green-600" />
Sales Experience & Availability
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="education">Education Level *</Label>
<Select value={formData.education} onValueChange={(value) => handleInputChange('education', value)}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select education level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="high-school">High School</SelectItem>
<SelectItem value="diploma">Diploma</SelectItem>
<SelectItem value="bachelor">Bachelor's Degree</SelectItem>
<SelectItem value="master">Master's Degree</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="experience">Work Experience *</Label>
<Select value={formData.experience} onValueChange={(value) => handleInputChange('experience', value)}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select experience level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="fresher">Fresher (0 years)</SelectItem>
<SelectItem value="1-2">1-2 years</SelectItem>
<SelectItem value="3-5">3-5 years</SelectItem>
<SelectItem value="5+">5+ years</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="preferredRole">Position *</Label>
<Select value={formData.preferredRole} onValueChange={(value) => handleInputChange('preferredRole', value)}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select position" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sales-executive">Sales Executive</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="availableHours">Available Hours per Day *</Label>
<Select value={formData.availableHours} onValueChange={(value) => handleInputChange('availableHours', value)}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select available hours" />
</SelectTrigger>
<SelectContent>
<SelectItem value="2-3">2-3 hours</SelectItem>
<SelectItem value="4-5">4-5 hours</SelectItem>
<SelectItem value="6-7">6-7 hours</SelectItem>
<SelectItem value="8+">8+ hours</SelectItem>
</SelectContent>
</Select>
</div>
<div className="md:col-span-2">
<Label htmlFor="availableDays">Available Days *</Label>
<Select value={formData.availableDays} onValueChange={(value) => handleInputChange('availableDays', value)}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select available days" />
</SelectTrigger>
<SelectContent>
<SelectItem value="weekdays">Weekdays (Mon-Fri)</SelectItem>
<SelectItem value="weekends">Weekends (Sat-Sun)</SelectItem>
<SelectItem value="all-days">All Days</SelectItem>
<SelectItem value="flexible">Flexible</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{/* Additional Information */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<FileText className="h-5 w-5 mr-2 text-green-600" />
Additional Information
</h3>
<div className="space-y-4">
<div>
<Label htmlFor="skills">Skills & Expertise</Label>
<Textarea
id="skills"
value={formData.skills}
onChange={(e) => handleInputChange('skills', e.target.value)}
placeholder="List your relevant skills, software knowledge, etc."
className="mt-1"
rows={3}
/>
</div>
<div>
<Label htmlFor="languagesKnown">Languages Known</Label>
<Input
id="languagesKnown"
value={formData.languagesKnown}
onChange={(e) => handleInputChange('languagesKnown', e.target.value)}
placeholder="e.g., Hindi, English, Regional languages"
className="mt-1"
/>
</div>
<div>
<Label htmlFor="previousWorkExperience">Previous Work Experience</Label>
<Textarea
id="previousWorkExperience"
value={formData.previousWorkExperience}
onChange={(e) => handleInputChange('previousWorkExperience', e.target.value)}
placeholder="Describe your previous work experience (if any)"
className="mt-1"
rows={3}
/>
</div>
<div>
<Label htmlFor="motivation">Why do you want to join our sales team? *</Label>
<Textarea
id="motivation"
value={formData.motivation}
onChange={(e) => handleInputChange('motivation', e.target.value)}
placeholder="Tell us about your interest in sales and what motivates you"
className="mt-1"
rows={4}
required
/>
</div>
</div>
</div>
{/* Submit Button */}
<div className="pt-6">
<Button
type="submit"
disabled={loading}
className="w-full bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 text-white font-semibold py-3 rounded-lg transition-all duration-200"
>
{loading ? (
<>
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
Submitting Application...
</>
) : (
<>
<Briefcase className="h-5 w-5 mr-2" />
Submit Application
</>
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
{/* Success Modal */}
<Dialog open={showSuccessModal} onOpenChange={setShowSuccessModal}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center text-green-600">
<CheckCircle className="h-6 w-6 mr-2" />
Application Submitted!
</DialogTitle>
<DialogDescription asChild>
<div className="space-y-4">
<p>Your Sales Executive application has been submitted successfully!</p>
{loginCredentials && (
<div className="bg-gray-50 p-4 rounded-lg">
<h4 className="font-semibold mb-2">Your Login Credentials:</h4>
<p><strong>Email:</strong> {loginCredentials.email}</p>
<p><strong>Password:</strong> {loginCredentials.password}</p>
<p className="text-sm text-gray-600 mt-2">
Please save these credentials. You'll receive a welcome email shortly.
</p>
</div>
)}
<div className="bg-green-50 p-4 rounded-lg">
<h4 className="font-semibold mb-2">What's Next?</h4>
<ul className="text-sm space-y-1">
<li> Our sales team will review your application</li>
<li> You'll receive a call within 2-3 business days</li>
<li> Complete the interview process</li>
<li> Start earning 10,000 + 1% commission!</li>
</ul>
</div>
</div>
</DialogDescription>
</DialogHeader>
<div className="flex justify-end">
<Button onClick={() => setShowSuccessModal(false)}>
Close
</Button>
</div>
</DialogContent>
</Dialog>
</>
)
}

View File

@@ -0,0 +1,438 @@
'use client'
import { useState, useEffect } from 'react'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { toast } from 'sonner'
import { Loader2, CheckCircle, Crown, Award, Star } from 'lucide-react'
import { signIn } from 'next-auth/react'
interface PartnershipApplicationFormProps {
isOpen: boolean
onClose: () => void
selectedTier?: string
}
export default function PartnershipApplicationForm({
isOpen,
onClose,
selectedTier = 'Silver'
}: PartnershipApplicationFormProps) {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
phone: '',
businessName: '',
businessType: '',
experience: '',
partnershipTier: selectedTier,
expectedCustomers: '',
address: '',
city: '',
state: '',
zipCode: '',
motivation: '',
marketingPlan: ''
})
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
// Update form data when selectedTier prop changes
useEffect(() => {
setFormData(prev => ({ ...prev, partnershipTier: selectedTier }))
}, [selectedTier])
// Reset form when modal opens
useEffect(() => {
if (isOpen) {
setFormData({
firstName: '',
lastName: '',
email: '',
phone: '',
businessName: '',
businessType: '',
experience: '',
partnershipTier: selectedTier,
expectedCustomers: '',
address: '',
city: '',
state: '',
zipCode: '',
motivation: '',
marketingPlan: ''
})
setIsSuccess(false)
setIsSubmitting(false)
}
}, [isOpen, selectedTier])
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
try {
const response = await fetch('/api/partnership/apply', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...formData,
expectedCustomers: parseInt(formData.expectedCustomers) || 0
}),
})
const result = await response.json()
if (result.success) {
setIsSuccess(true)
toast.success('Application submitted successfully! Check your email for login details.')
// Auto-login the user after successful registration
setTimeout(async () => {
try {
await signIn('credentials', {
email: formData.email,
redirect: false
})
window.location.href = '/dashboard'
} catch (error) {
console.error('Auto-login failed:', error)
}
}, 2000)
} else {
toast.error(result.message || 'Application failed. Please try again.')
}
} catch (error) {
console.error('Application error:', error)
toast.error('Something went wrong. Please try again.')
} finally {
setIsSubmitting(false)
}
}
const getTierIcon = (tier: string) => {
switch (tier) {
case 'Diamond': return Crown
case 'Gold': return Award
case 'Silver': return Star
default: return Star
}
}
const getTierColor = (tier: string) => {
switch (tier) {
case 'Diamond': return '#3B82F6'
case 'Gold': return '#F59E0B'
case 'Silver': return '#8B5CF6'
default: return '#8B5CF6'
}
}
const getTierMaxCustomers = (tier: string) => {
switch (tier) {
case 'Diamond': return 3000
case 'Gold': return 1500
case 'Silver': return 500
default: return 500
}
}
if (isSuccess) {
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<div className="text-center py-8">
<CheckCircle className="h-16 w-16 text-green-500 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-900 mb-2">
Application Submitted!
</h3>
<p className="text-gray-600 mb-4">
Welcome to Padmaaja Rasooi! Check your email for login details.
</p>
<p className="text-sm text-gray-500 mb-6">
You'll be automatically logged in and redirected to your dashboard.
</p>
<Button onClick={onClose} className="w-full">
Close
</Button>
</div>
</DialogContent>
</Dialog>
)
}
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-2xl font-bold text-center">
Partnership Application
</DialogTitle>
</DialogHeader>
{/* Selected Tier Display */}
<Card className="mb-6">
<CardHeader className="pb-3">
<div className="flex items-center justify-center space-x-3">
{(() => {
const TierIcon = getTierIcon(formData.partnershipTier)
return (
<div
className="w-12 h-12 rounded-full flex items-center justify-center"
style={{backgroundColor: `${getTierColor(formData.partnershipTier)}20`}}
>
<TierIcon
className="h-6 w-6"
style={{color: getTierColor(formData.partnershipTier)}}
/>
</div>
)
})()}
<div>
<h3 className="text-lg font-semibold">{formData.partnershipTier} Partner</h3>
<p className="text-sm text-gray-600">
Max {getTierMaxCustomers(formData.partnershipTier).toLocaleString()} customers
</p>
</div>
</div>
</CardHeader>
</Card>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Personal Information */}
<Card>
<CardHeader>
<CardTitle>Personal Information</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="firstName">First Name *</Label>
<Input
id="firstName"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="lastName">Last Name *</Label>
<Input
id="lastName"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="email">Email Address *</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="phone">Phone Number *</Label>
<Input
id="phone"
type="tel"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
required
/>
</div>
</CardContent>
</Card>
{/* Business Information */}
<Card>
<CardHeader>
<CardTitle>Business Information</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="businessName">Business Name (Optional)</Label>
<Input
id="businessName"
value={formData.businessName}
onChange={(e) => handleInputChange('businessName', e.target.value)}
/>
</div>
<div>
<Label htmlFor="businessType">Business Type *</Label>
<Select value={formData.businessType} onValueChange={(value) => handleInputChange('businessType', value)}>
<SelectTrigger>
<SelectValue placeholder="Select business type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="individual">Individual</SelectItem>
<SelectItem value="retail">Retail Store</SelectItem>
<SelectItem value="restaurant">Restaurant/Hotel</SelectItem>
<SelectItem value="distributor">Distributor</SelectItem>
<SelectItem value="online">Online Business</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</div>
<div className="md:col-span-2">
<Label htmlFor="experience">Experience in Food/Retail Business *</Label>
<Select value={formData.experience} onValueChange={(value) => handleInputChange('experience', value)}>
<SelectTrigger>
<SelectValue placeholder="Select your experience level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="beginner">Beginner (0-1 years)</SelectItem>
<SelectItem value="intermediate">Intermediate (2-5 years)</SelectItem>
<SelectItem value="experienced">Experienced (5+ years)</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Partnership Details */}
<Card>
<CardHeader>
<CardTitle>Partnership Details</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="partnershipTier">Partnership Tier *</Label>
<Select value={formData.partnershipTier} onValueChange={(value) => handleInputChange('partnershipTier', value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Silver">Silver (500 customers)</SelectItem>
<SelectItem value="Gold">Gold (1,500 customers)</SelectItem>
<SelectItem value="Diamond">Diamond (3,000 customers)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="expectedCustomers">Expected Customers in First Year</Label>
<Input
id="expectedCustomers"
type="number"
value={formData.expectedCustomers}
onChange={(e) => handleInputChange('expectedCustomers', e.target.value)}
max={getTierMaxCustomers(formData.partnershipTier)}
/>
</div>
</CardContent>
</Card>
{/* Address Information */}
<Card>
<CardHeader>
<CardTitle>Address Information</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2">
<Label htmlFor="address">Street Address *</Label>
<Input
id="address"
value={formData.address}
onChange={(e) => handleInputChange('address', e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="city">City *</Label>
<Input
id="city"
value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="state">State *</Label>
<Input
id="state"
value={formData.state}
onChange={(e) => handleInputChange('state', e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="zipCode">ZIP Code *</Label>
<Input
id="zipCode"
value={formData.zipCode}
onChange={(e) => handleInputChange('zipCode', e.target.value)}
required
/>
</div>
</CardContent>
</Card>
{/* Additional Information */}
<Card>
<CardHeader>
<CardTitle>Additional Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="motivation">Why do you want to become a partner? *</Label>
<Textarea
id="motivation"
value={formData.motivation}
onChange={(e) => handleInputChange('motivation', e.target.value)}
required
rows={3}
/>
</div>
<div>
<Label htmlFor="marketingPlan">How do you plan to market our products? *</Label>
<Textarea
id="marketingPlan"
value={formData.marketingPlan}
onChange={(e) => handleInputChange('marketingPlan', e.target.value)}
required
rows={3}
/>
</div>
</CardContent>
</Card>
{/* Submit Button */}
<div className="flex justify-end space-x-4">
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="min-w-[120px]"
>
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Submitting...
</>
) : (
'Submit Application'
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,465 @@
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
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 { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Store,
User,
Mail,
Phone,
MapPin,
Building,
FileText,
CheckCircle,
Loader2,
Gift
} from 'lucide-react'
import { toast } from 'sonner'
interface WholesalerRegistrationFormProps {
isOpen: boolean
onClose: () => void
}
interface WholesalerFormData {
// Business Information
businessName: string
businessType: string
gstNumber: string
panNumber: string
// Personal Information
firstName: string
lastName: string
email: string
phone: string
// Address Information
address: string
city: string
state: string
zipCode: string
// Business Details
experience: string
expectedOrderVolume: string
productCategories: string
businessDescription: string
}
export default function WholesalerRegistrationForm({ isOpen, onClose }: WholesalerRegistrationFormProps) {
const [formData, setFormData] = useState<WholesalerFormData>({
businessName: '',
businessType: '',
gstNumber: '',
panNumber: '',
firstName: '',
lastName: '',
email: '',
phone: '',
address: '',
city: '',
state: '',
zipCode: '',
experience: '',
expectedOrderVolume: '',
productCategories: '',
businessDescription: ''
})
const [loading, setLoading] = useState(false)
const [showSuccessModal, setShowSuccessModal] = useState(false)
const [loginCredentials, setLoginCredentials] = useState<{
email: string
password: string
} | null>(null)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
try {
const response = await fetch('/api/wholesaler/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || 'Registration failed')
}
setLoginCredentials(data.loginCredentials)
setShowSuccessModal(true)
onClose() // Close the main registration modal
// Reset form
setFormData({
businessName: '',
businessType: '',
gstNumber: '',
panNumber: '',
firstName: '',
lastName: '',
email: '',
phone: '',
address: '',
city: '',
state: '',
zipCode: '',
experience: '',
expectedOrderVolume: '',
productCategories: '',
businessDescription: ''
})
toast.success('Wholesaler registration successful!')
} catch (error) {
console.error('Registration error:', error)
toast.error(error instanceof Error ? error.message : 'Registration failed')
} finally {
setLoading(false)
}
}
const handleInputChange = (field: keyof WholesalerFormData, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
return (
<>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-2xl flex items-center">
<Building className="h-6 w-6 mr-3" />
Wholesaler Registration
</DialogTitle>
<DialogDescription>
Join our network and get exclusive access to premium products with 25% wholesale discount on bulk orders
</DialogDescription>
</DialogHeader>
<div className="flex items-center justify-center gap-4 mb-6">
<Badge className="bg-green-100 text-green-800 px-4 py-2">
<Gift className="h-4 w-4 mr-2" />
25% Discount
</Badge>
<Badge className="bg-blue-100 text-blue-800 px-4 py-2">
<Store className="h-4 w-4 mr-2" />
Bulk Orders
</Badge>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Business Information */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<Building className="h-5 w-5 mr-2 text-blue-600" />
Business Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label htmlFor="businessName">Business Name *</Label>
<Input
id="businessName"
value={formData.businessName}
onChange={(e) => handleInputChange('businessName', e.target.value)}
placeholder="Enter your business name"
required
/>
</div>
<div>
<Label htmlFor="businessType">Business Type *</Label>
<Select onValueChange={(value) => handleInputChange('businessType', value)}>
<SelectTrigger>
<SelectValue placeholder="Select business type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="retail">Retail Store</SelectItem>
<SelectItem value="distributor">Distributor</SelectItem>
<SelectItem value="restaurant">Restaurant/Hotel</SelectItem>
<SelectItem value="supermarket">Supermarket</SelectItem>
<SelectItem value="export">Export Business</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="gstNumber">GST Number</Label>
<Input
id="gstNumber"
value={formData.gstNumber}
onChange={(e) => handleInputChange('gstNumber', e.target.value)}
placeholder="Enter GST number"
/>
</div>
<div>
<Label htmlFor="panNumber">PAN Number</Label>
<Input
id="panNumber"
value={formData.panNumber}
onChange={(e) => handleInputChange('panNumber', e.target.value)}
placeholder="Enter PAN number"
/>
</div>
</div>
</div>
{/* Personal Information */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<User className="h-5 w-5 mr-2 text-blue-600" />
Personal Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label htmlFor="firstName">First Name *</Label>
<Input
id="firstName"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
placeholder="Enter your first name"
required
/>
</div>
<div>
<Label htmlFor="lastName">Last Name *</Label>
<Input
id="lastName"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
placeholder="Enter your last name"
required
/>
</div>
<div>
<Label htmlFor="email">Email Address *</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="Enter your email address"
required
/>
</div>
<div>
<Label htmlFor="phone">Phone Number *</Label>
<Input
id="phone"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
placeholder="Enter your phone number"
required
/>
</div>
</div>
</div>
{/* Address Information */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<MapPin className="h-5 w-5 mr-2 text-blue-600" />
Address Information
</h3>
<div className="grid grid-cols-1 gap-6">
<div>
<Label htmlFor="address">Business Address *</Label>
<Textarea
id="address"
value={formData.address}
onChange={(e) => handleInputChange('address', e.target.value)}
placeholder="Enter your complete business address"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<Label htmlFor="city">City *</Label>
<Input
id="city"
value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)}
placeholder="Enter city"
required
/>
</div>
<div>
<Label htmlFor="state">State *</Label>
<Input
id="state"
value={formData.state}
onChange={(e) => handleInputChange('state', e.target.value)}
placeholder="Enter state"
required
/>
</div>
<div>
<Label htmlFor="zipCode">ZIP Code *</Label>
<Input
id="zipCode"
value={formData.zipCode}
onChange={(e) => handleInputChange('zipCode', e.target.value)}
placeholder="Enter ZIP code"
required
/>
</div>
</div>
</div>
</div>
{/* Business Details */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<FileText className="h-5 w-5 mr-2 text-blue-600" />
Business Details
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label htmlFor="experience">Business Experience</Label>
<Select onValueChange={(value) => handleInputChange('experience', value)}>
<SelectTrigger>
<SelectValue placeholder="Select experience" />
</SelectTrigger>
<SelectContent>
<SelectItem value="startup">Startup (0-1 years)</SelectItem>
<SelectItem value="growing">Growing (1-3 years)</SelectItem>
<SelectItem value="established">Established (3-5 years)</SelectItem>
<SelectItem value="mature">Mature (5+ years)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="expectedOrderVolume">Expected Monthly Order Volume</Label>
<Select onValueChange={(value) => handleInputChange('expectedOrderVolume', value)}>
<SelectTrigger>
<SelectValue placeholder="Select volume" />
</SelectTrigger>
<SelectContent>
<SelectItem value="small">Small (1L - 5L)</SelectItem>
<SelectItem value="medium">Medium (5L - 20L)</SelectItem>
<SelectItem value="large">Large (20L - 50L)</SelectItem>
<SelectItem value="enterprise">Enterprise (50L+)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="mt-6">
<Label htmlFor="productCategories">Product Categories of Interest</Label>
<Input
id="productCategories"
value={formData.productCategories}
onChange={(e) => handleInputChange('productCategories', e.target.value)}
placeholder="e.g., Basmati Rice, Non-Basmati Rice, Organic Rice"
/>
</div>
<div className="mt-6">
<Label htmlFor="businessDescription">Business Description</Label>
<Textarea
id="businessDescription"
value={formData.businessDescription}
onChange={(e) => handleInputChange('businessDescription', e.target.value)}
placeholder="Tell us about your business, target market, and goals"
rows={4}
/>
</div>
</div>
<div className="bg-blue-50 p-6 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-2">Wholesaler Benefits:</h4>
<ul className="text-sm text-gray-600 space-y-1">
<li> 25% discount on all bulk orders</li>
<li> Dedicated account manager</li>
<li> Priority customer support</li>
<li> Flexible payment terms</li>
<li> Quality guarantee on all products</li>
</ul>
</div>
<Button
type="submit"
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white py-3 text-lg"
disabled={loading}
>
{loading ? (
<>
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
Registering...
</>
) : (
<>
<Store className="h-5 w-5 mr-2" />
Register as Wholesaler
</>
)}
</Button>
</form>
</DialogContent>
</Dialog>
{/* Success Modal */}
<Dialog open={showSuccessModal} onOpenChange={setShowSuccessModal}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center text-green-600">
<CheckCircle className="h-6 w-6 mr-2" />
Registration Successful!
</DialogTitle>
<DialogDescription asChild>
<div className="space-y-4">
<p>Your wholesaler account has been created successfully!</p>
{loginCredentials && (
<div className="bg-gray-50 p-4 rounded-lg">
<h4 className="font-semibold mb-2">Your Login Credentials:</h4>
<div className="space-y-2 text-sm">
<div>
<span className="font-medium">Email:</span> {loginCredentials.email}
</div>
<div>
<span className="font-medium">Password:</span> {loginCredentials.password}
</div>
</div>
<p className="text-xs text-gray-600 mt-2">
Please save these credentials and change your password after first login.
</p>
</div>
)}
<div className="text-sm text-gray-600">
<p> You will receive a confirmation email shortly</p>
<p> Our team will contact you within 24 hours</p>
<p> You can now access 25% wholesale discounts</p>
</div>
</div>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</>
)
}

View File

@@ -0,0 +1,193 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { motion, AnimatePresence } from 'framer-motion'
import { Wheat, Leaf, ShoppingBag, Building2, Settings, LucideIcon } from 'lucide-react'
interface MegaMenuItem {
name: string
href: string
description: string
}
interface MegaMenuCategory {
name: string
id?: string
icon: LucideIcon
items: MegaMenuItem[]
}
interface MegaMenuData {
title: string
type: 'categories'
categories: MegaMenuCategory[]
}
interface MegaMenuProps {
menuKey: string
menu: MegaMenuData
isActive: boolean
onClose: () => void
}
export default function MegaMenu({ menuKey, menu, isActive, onClose }: MegaMenuProps) {
const [latestProducts, setLatestProducts] = useState<MegaMenuItem[]>([])
const [isLoadingProducts, setIsLoadingProducts] = useState(true)
// Fetch 4 latest products for Products menu on component mount
useEffect(() => {
if (menuKey === 'products') {
const fetchLatestProducts = async () => {
try {
setIsLoadingProducts(true)
// Fetch latest 4 products directly
const productsResponse = await fetch('/api/products?limit=4&sort=latest')
const productsData = await productsResponse.json()
if (productsData && productsData.products) {
const products = productsData.products.map((product: any) => ({
name: product.name,
href: `/products/${product.slug}`,
description: product.description?.substring(0, 50) + '...' || 'Premium quality product'
}))
setLatestProducts(products)
}
} catch (error) {
console.error('Error fetching latest products:', error)
// Fallback to static data if API fails
setLatestProducts([
{ name: 'Premium Basmati Rice', href: '/products', description: 'Aromatic long-grain rice' },
{ name: 'Kashmina Rice', href: '/products', description: 'Premium quality rice' },
{ name: 'Non-Basmati Rice', href: '/products', description: 'Traditional varieties' },
{ name: 'Multigrain Flour', href: '/products', description: 'Nutritious blend' },
])
} finally {
setIsLoadingProducts(false)
}
}
fetchLatestProducts()
} else {
setIsLoadingProducts(false)
}
}, [menuKey]) // Only depend on menuKey, not isActive
return (
<AnimatePresence>
{isActive && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.2 }}
className="absolute top-full mt-2 bg-white rounded-xl shadow-2xl border border-slate-200/50 backdrop-blur-lg overflow-hidden z-50 right-0"
style={{
width: 'auto',
minWidth: menuKey === 'products' ? '320px' : '280px',
maxWidth: '90vw'
}}
>
<div className="p-3 sm:p-4">
{menuKey === 'products' ? (
// Latest Products - Simple List
isLoadingProducts ? (
<div className="space-y-2">
<div className="flex items-center space-x-2 pb-1.5 border-b border-slate-100">
<div className="w-4 h-4 bg-slate-200 rounded animate-pulse"></div>
<div className="h-4 bg-slate-200 rounded w-32 animate-pulse"></div>
</div>
<div className="space-y-1">
{[1, 2, 3, 4].map((item) => (
<div key={item} className="p-2">
<div className="h-3 bg-slate-200 rounded w-full mb-1 animate-pulse"></div>
<div className="h-2 bg-slate-100 rounded w-3/4 animate-pulse"></div>
</div>
))}
</div>
</div>
) : (
<div className="space-y-2">
<div className="flex items-center space-x-2 pb-1.5 border-b border-slate-100">
<ShoppingBag className="w-4 h-4 text-emerald-600 flex-shrink-0" />
<h3 className="font-semibold text-slate-800 text-sm">Latest Products</h3>
</div>
<div className="space-y-1">
{latestProducts.map((product, idx) => (
<Link
key={idx}
href={product.href}
className="block p-2 rounded-lg hover:bg-slate-50 transition-colors duration-200 group"
onClick={onClose}
>
<div className="font-medium text-slate-800 group-hover:text-emerald-600 transition-colors duration-200 text-sm truncate">
{product.name}
</div>
<div className="text-xs text-slate-500 truncate">
{product.description}
</div>
</Link>
))}
</div>
</div>
)
) : (
// Other menu types - Categories
menu.type === 'categories' && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
{menu.categories?.map((category, idx) => {
const IconComponent = category.icon
return (
<div key={idx} className="space-y-2 sm:space-y-3">
<div className="flex items-center space-x-2 pb-1.5 border-b border-slate-100">
<IconComponent className="w-4 h-4 text-emerald-600 flex-shrink-0" />
<h3 className="font-semibold text-slate-800 text-sm truncate">{category.name}</h3>
</div>
<div className="space-y-1">
{category.items?.map((item, itemIdx) => (
<Link
key={itemIdx}
href={item.href}
className="block p-2 rounded-lg hover:bg-slate-50 transition-colors duration-200 group"
onClick={onClose}
>
<div className="font-medium text-slate-800 group-hover:text-emerald-600 transition-colors duration-200 text-sm truncate">
{item.name}
</div>
<div className="text-xs text-slate-500 truncate">
{item.description}
</div>
</Link>
))}
</div>
</div>
)
})}
</div>
)
)}
{/* View All Products Button - Only for Products Menu */}
{menuKey === 'products' && !isLoadingProducts && (
<div className="mt-4 pt-3 border-t border-slate-100">
<Link
href="/products"
className="block w-full p-3 rounded-lg bg-gradient-to-r from-emerald-600 to-blue-600 text-white hover:from-emerald-700 hover:to-blue-700 transition-all duration-200 group text-center font-medium"
onClick={onClose}
>
<div className="flex items-center justify-center space-x-2">
<ShoppingBag className="w-4 h-4" />
<span>View All Products</span>
</div>
</Link>
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
)
}

View File

@@ -0,0 +1,191 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import {
Home,
ShoppingBag,
ShoppingCart,
User,
MoreHorizontal
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import CartSidebar from '@/components/shop/CartSidebar'
import { cartManager } from '@/lib/cart'
const tabs = [
{
id: 'home',
label: 'Home',
icon: Home,
href: '/',
activeRoutes: ['/']
},
{
id: 'products',
label: 'Products',
icon: ShoppingBag,
href: '/products',
activeRoutes: ['/products', '/products/[slug]']
},
{
id: 'cart',
label: 'Cart',
icon: ShoppingCart,
href: '/cart',
activeRoutes: ['/cart', '/checkout']
},
{
id: 'account',
label: 'Account',
icon: User,
href: '/dashboard',
activeRoutes: ['/dashboard']
},
// {
// id: 'more',
// label: 'More',
// icon: MoreHorizontal,
// href: '/more',
// activeRoutes: ['/more', '/about', '/contact', '/partnership', '/bulk-supply']
// }
]
export default function MobileTabBar() {
const pathname = usePathname()
const [isVisible, setIsVisible] = useState(true)
const [lastScrollY, setLastScrollY] = useState(0)
const [cartItemCount, setCartItemCount] = useState(0)
// Load cart count
const loadCartCount = () => {
const cart = cartManager.getCart()
const count = cart.reduce((sum, item) => sum + item.quantity, 0)
setCartItemCount(count)
}
useEffect(() => {
loadCartCount()
const handleCartUpdate = () => loadCartCount()
window.addEventListener('cartUpdated', handleCartUpdate)
return () => {
window.removeEventListener('cartUpdated', handleCartUpdate)
}
}, [])
// Hide/show tab bar on scroll
useEffect(() => {
const handleScroll = () => {
const currentScrollY = window.scrollY
if (currentScrollY > lastScrollY && currentScrollY > 100) {
setIsVisible(false) // Hide when scrolling down
} else {
setIsVisible(true) // Show when scrolling up
}
setLastScrollY(currentScrollY)
}
window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
}, [lastScrollY])
const isActiveTab = (tab: typeof tabs[0]) => {
if (tab.activeRoutes.includes(pathname)) return true
return tab.activeRoutes.some(route => {
if (route.includes('[slug]')) {
const baseRoute = route.replace('/[slug]', '')
return pathname.startsWith(baseRoute) && pathname !== baseRoute
}
return false
})
}
return (
<div
className={cn(
"fixed bottom-0 left-0 right-0 z-50 bg-white border-t border-gray-200 shadow-lg transition-transform duration-300 md:hidden",
isVisible ? "translate-y-0" : "translate-y-full"
)}
>
<div className="flex items-center justify-around px-2 py-2 safe-area-bottom">
{tabs.map((tab) => {
const Icon = tab.icon
const isActive = isActiveTab(tab)
// Special handling for cart tab
if (tab.id === 'cart') {
return (
<CartSidebar key={tab.id}>
<div
className={cn(
"flex flex-col items-center justify-center py-2 px-3 rounded-lg transition-all duration-200 min-w-0 flex-1 relative cursor-pointer",
"text-gray-600 hover:text-emerald-600 hover:bg-gray-50"
)}
>
<div className="relative">
<Icon
className="h-5 w-5 mb-1 transition-transform duration-200"
/>
{cartItemCount > 0 && (
<Badge
className="absolute -top-2 -right-2 h-4 w-4 flex items-center justify-center p-0 bg-red-500 text-white !text-xs"
>
{cartItemCount > 99 ? '99+' : cartItemCount}
</Badge>
)}
</div>
<span
className="text-xs font-medium truncate transition-colors duration-200 text-gray-600"
>
{tab.label}
</span>
</div>
</CartSidebar>
)
}
// Regular tabs
return (
<Link
key={tab.id}
href={tab.href}
className={cn(
"flex flex-col items-center justify-center py-2 px-3 rounded-lg transition-all duration-200 min-w-0 flex-1",
isActive
? "text-emerald-600 bg-emerald-50"
: "text-gray-600 hover:text-emerald-600 hover:bg-gray-50"
)}
>
<Icon
className={cn(
"h-5 w-5 mb-1 transition-transform duration-200",
isActive ? "scale-110" : ""
)}
/>
<span
className={cn(
"text-xs font-medium truncate transition-colors duration-200",
isActive ? "text-emerald-700" : "text-gray-600"
)}
>
{tab.label}
</span>
{isActive && (
<div className="absolute -top-0.5 left-1/2 transform -translate-x-1/2 w-8 h-0.5 bg-emerald-600 rounded-full" />
)}
</Link>
)
})}
</div>
{/* Safe area padding for devices with home indicator */}
<div className="h-safe-area-inset-bottom bg-white" />
</div>
)
}

View File

@@ -0,0 +1,145 @@
import Link from 'next/link'
import { Linkedin, Facebook, Instagram, Mail, Phone, MapPin, Award, Truck, Shield } from 'lucide-react'
export function Footer() {
return (
<footer className="bg-gray-900 text-white">
{/* Main Footer Content */}
<div className="max-w-7xl mx-auto px-3 sm:px-6 lg:px-8 py-8 sm:py-12 lg:py-16">
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6 sm:gap-8">
{/* Company Info - Full width on mobile */}
<div className="lg:col-span-2 mb-6 lg:mb-0">
<div className="mb-4 sm:mb-6">
<h3 className="text-xl sm:text-2xl font-bold text-orange-400 mb-2">Padmaaja</h3>
<p className="text-gray-400 text-sm sm:text-base leading-relaxed">
India's leading manufacturer and exporter of premium Basmati rice and multigrain flour.
Committed to delivering authentic quality and taste that has been trusted for generations.
</p>
</div>
{/* Contact Info */}
<div className="space-y-2 sm:space-y-3">
<div className="flex items-start space-x-3">
<MapPin className="h-4 w-4 sm:h-5 sm:w-5 text-orange-400 mt-1 flex-shrink-0" />
<div className="text-xs sm:text-sm">
<p className="font-medium">Corporate Office</p>
<p className="text-gray-400">11B/79, Vrindavan Colony, Lucknow - 226029</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Phone className="h-4 w-4 sm:h-5 sm:w-5 text-orange-400" />
<div className="text-xs sm:text-sm">
<a href="tel:+91-9475758817" className="hover:text-orange-400 transition-colors">
+91-9475758817
</a>
</div>
</div>
<div className="flex items-center space-x-3">
<Mail className="h-4 w-4 sm:h-5 sm:w-5 text-orange-400" />
<div className="text-xs sm:text-sm">
<a href="mailto:info@padmajarice.com" className="hover:text-orange-400 transition-colors">
info@padmajarice.com
</a>
</div>
</div>
</div>
</div>
{/* Menu sections - Responsive layout */}
<div className="lg:col-span-3">
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-8">
{/* Quick Links */}
<div>
<h4 className="font-semibold text-base sm:text-lg mb-3 sm:mb-4 text-white">Quick Links</h4>
<ul className="space-y-1.5 sm:space-y-2 text-xs sm:text-sm">
<li><Link href="/about" className="text-gray-400 hover:text-orange-400 transition-colors">About Us</Link></li>
<li><Link href="/products" className="text-gray-400 hover:text-orange-400 transition-colors">Our Products</Link></li>
<li><Link href="/partnership" className="text-gray-400 hover:text-orange-400 transition-colors">Partnership</Link></li>
<li><Link href="/wholesaler" className="text-gray-400 hover:text-orange-400 transition-colors">Become Wholesaler</Link></li>
<li><Link href="/contact" className="text-gray-400 hover:text-orange-400 transition-colors">Contact Us</Link></li>
</ul>
</div>
{/* Business Solutions */}
<div>
<h4 className="font-semibold text-base sm:text-lg mb-3 sm:mb-4 text-white">Business</h4>
<ul className="space-y-1.5 sm:space-y-2 text-xs sm:text-sm">
<li><Link href="/wholesaler" className="text-gray-400 hover:text-orange-400 transition-colors">Distributor Program</Link></li>
<li><Link href="/partnership" className="text-gray-400 hover:text-orange-400 transition-colors">Business Partnership</Link></li>
<li><Link href="/contact" className="text-gray-400 hover:text-orange-400 transition-colors">Private Label</Link></li>
<li><Link href="/contact" className="text-gray-400 hover:text-orange-400 transition-colors">Export Inquiry</Link></li>
<li><Link href="/contact" className="text-gray-400 hover:text-orange-400 transition-colors">Franchise</Link></li>
</ul>
</div>
{/* Legal & Support - Responsive column spanning */}
<div className="col-span-2 lg:col-span-1">
<h4 className="font-semibold text-base sm:text-lg mb-3 sm:mb-4 text-white">Support</h4>
<ul className="space-y-1.5 sm:space-y-2 text-xs sm:text-sm grid grid-cols-2 lg:grid-cols-1 gap-x-4 lg:gap-x-0">
<li><Link href="/orders" className="text-gray-400 hover:text-orange-400 transition-colors">Track Order</Link></li>
<li><Link href="/profile" className="text-gray-400 hover:text-orange-400 transition-colors">My Account</Link></li>
<li><Link href="/terms-of-service" className="text-gray-400 hover:text-orange-400 transition-colors">Terms of Service</Link></li>
<li><Link href="/privacy-policy" className="text-gray-400 hover:text-orange-400 transition-colors">Privacy Policy</Link></li>
<li><Link href="/refund-policy" className="text-gray-400 hover:text-orange-400 transition-colors">Refund Policy</Link></li>
</ul>
</div>
</div>
</div>
</div>
{/* Social Media & Newsletter */}
{/* <div className="mt-6 sm:mt-8 pt-6 sm:pt-8 border-t border-gray-800">
<div className="flex flex-col space-y-4 sm:space-y-6 lg:space-y-0 lg:flex-row lg:justify-between lg:items-center"> */}
{/* Social Media */}
{/* <div className="flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0 sm:space-x-4 lg:space-x-6">
<span className="text-sm font-medium text-gray-400">Follow Us:</span>
<div className="flex space-x-4">
<Link href="#" className="text-gray-400 hover:text-orange-400 transition-colors">
<Linkedin className="h-5 w-5 sm:h-6 sm:w-6" />
</Link>
<Link href="#" className="text-gray-400 hover:text-orange-400 transition-colors">
<Facebook className="h-5 w-5 sm:h-6 sm:w-6" />
</Link>
<Link href="#" className="text-gray-400 hover:text-orange-400 transition-colors">
<Instagram className="h-5 w-5 sm:h-6 sm:w-6" />
</Link>
</div>
</div> */}
{/* Newsletter Signup */}
{/* <div className="flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0 sm:space-x-3">
<span className="text-sm font-medium text-gray-400">Stay Updated:</span>
<div className="flex w-full sm:w-auto">
<input
type="email"
placeholder="Enter your email"
className="flex-1 sm:flex-none px-3 py-2 bg-gray-800 border border-gray-700 rounded-l-md text-sm focus:outline-none focus:border-orange-400 text-white placeholder-gray-500 w-full sm:w-48"
/>
<button className="px-3 sm:px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white text-sm rounded-r-md transition-colors">
Subscribe
</button>
</div>
</div> */}
{/* </div>
</div> */}
</div>
{/* Bottom Bar */}
<div className="bg-gray-950">
<div className="max-w-7xl mx-auto px-3 sm:px-6 lg:px-8 py-3 sm:py-4">
<div className="flex flex-col sm:flex-row justify-between items-center text-xs sm:text-sm text-gray-400 space-y-2 sm:space-y-0">
<div>
<p>&copy; 2024 Padmaaja. All rights reserved.</p>
</div>
<div>
<p>Crafted with ❤️ by <Link href='https://web.jabin.org' target='_blank'>Jabin Web</Link></p>
</div>
</div>
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,366 @@
'use client'
import Link from 'next/link'
import Image from 'next/image'
import { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { ChevronDown, Menu, X, Wheat, Leaf, Package, Building2, Settings, ShoppingBag, LogOut, LucideIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { useSession, signOut } from 'next-auth/react'
import CartSidebar from '@/components/shop/CartSidebar'
import MegaMenu from '@/components/layout/MegaMenu'
import { isFeatureEnabled } from '@/lib/business-config'
interface MenuItem {
name: string
href: string
description: string
}
interface MenuCategory {
name: string
icon: LucideIcon
items: MenuItem[]
}
interface MegaMenuConfig {
title: string
type: 'categories'
categories: MenuCategory[]
}
export function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [activeDropdown, setActiveDropdown] = useState<string | null>(null)
const [isScrolled, setIsScrolled] = useState(false)
const navItems = [
// { name: 'Home', href: '/' },
{ name: 'Recipes', href: '/recipes' },
// { name: 'About', href: '/about' },
{ name: 'Contact', href: '/contact' },
]
const megaMenus: Record<string, MegaMenuConfig> = {
products: {
title: 'Products',
type: 'categories' as const,
categories: [] // Will be populated dynamically by MegaMenu component
},
company: {
title: 'Company',
type: 'categories' as const,
categories: [
{
name: 'About Us',
icon: Building2,
items: [
{ name: 'Our Story', href: '/about', description: 'Company history and mission' },
{ name: 'Our Founder', href: '/about/founder', description: 'Meet our visionary leader' },
{ name: 'Certifications', href: '/about/certifications', description: 'Industry recognition' },
]
},
{
name: 'Business',
icon: Settings,
items: [
{ name: 'Partnership', href: '/partnership', description: 'Join our network' },
{ name: 'Wholesaler', href: '/wholesaler', description: 'Wholesaler program' },
{ name: 'Sustainability', href: '/sustainability', description: 'Environmental commitment' },
]
},
]
}
}
const { data: session } = useSession()
const adminNavigation = [
{ name: 'Admin Dashboard', href: '/admin', icon: Settings },
{ name: 'Manage Products', href: '/admin/products', icon: Package },
{ name: 'Manage Orders', href: '/admin/orders', icon: ShoppingBag },
]
const memberNavigation = [
{ name: 'Dashboard', href: '/dashboard', icon: Settings },
{ name: 'My Orders', href: '/dashboard/orders', icon: ShoppingBag },
{ name: 'Profile', href: '/dashboard/profile', icon: Settings },
]
// Scroll detection for sticky header
useEffect(() => {
const handleScroll = () => {
const scrollTop = window.scrollY
const scrolled = scrollTop > 0
setIsScrolled(scrolled)
// Add/remove class to body for content padding
if (scrolled) {
document.body.classList.add('header-sticky')
} else {
document.body.classList.remove('header-sticky')
}
}
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
document.body.classList.remove('header-sticky')
}
}, [])
useEffect(() => {
if (mobileMenuOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = 'unset'
}
return () => {
document.body.style.overflow = 'unset'
}
}, [mobileMenuOpen])
const handleMouseEnter = (dropdown: string) => {
setActiveDropdown(dropdown)
}
const handleMouseLeave = () => {
setActiveDropdown(null)
}
const handleMobileMenuClick = () => {
setMobileMenuOpen(!mobileMenuOpen)
}
const handleMobileLinkClick = () => {
setMobileMenuOpen(false)
}
return (
<nav className={`transition-all duration-300 ${
isScrolled
? 'fixed top-0 left-0 right-0 z-50 bg-white/95 backdrop-blur-md border-b border-slate-200 shadow-lg'
: 'relative bg-white border-b border-slate-200'
}`}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className={`flex justify-between items-center transition-all duration-300 ${
isScrolled ? 'h-20' : 'h-24'
}`}>
{/* Logo */}
<Link href="/" className="flex-shrink-0 flex items-center z-10">
<div className="flex items-center space-x-3">
<Image
src="/kashmina-logo.png"
alt="PADMAAJA RASOOI PVT. LTD."
width={150}
height={150}
className="object-contain"
/>
{/* <div className="flex flex-col">
<span className="text-lg font-bold text-slate-800 transition-colors duration-300 leading-tight">
KASHMINA RICE
</span>
<span className="text-xs uppercase text-slate-600 transition-colors duration-300 leading-tight">
&quot;Premium Rice&quot;
</span>
</div> */}
</div>
</Link>
{/* Desktop Navigation */}
<div className="hidden lg:flex items-center space-x-8">
{/* Regular Nav Items */}
{navItems.map(item => (
<Link
key={item.name}
href={item.href}
className="text-base font-medium text-slate-700 hover:text-emerald-600 transition-colors duration-200"
>
{item.name}
</Link>
))}
{/* Mega Menu Items */}
{Object.entries(megaMenus).map(([key, menu]) => (
<div
key={key}
className="relative"
onMouseEnter={() => handleMouseEnter(key)}
onMouseLeave={handleMouseLeave}
>
<button
className={`flex items-center space-x-1 text-base font-medium text-slate-700 hover:text-emerald-600 transition-colors duration-200 ${
activeDropdown === key ? 'text-emerald-600' : ''
}`}
>
<span>{menu.title}</span>
<ChevronDown className={`w-4 h-4 transition-transform duration-200 ${
activeDropdown === key ? 'rotate-180' : ''
}`} />
</button>
{/* Mega Menu Dropdown */}
<MegaMenu
menuKey={key}
menu={menu}
isActive={activeDropdown === key}
onClose={() => setActiveDropdown(null)}
/>
</div>
))}
<div className="flex items-center space-x-4">
{/* B2C Feature - Cart Sidebar (Disabled for B2B mode) */}
{isFeatureEnabled('cart') && <CartSidebar />}
{session ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-10 w-10 rounded-full">
<Avatar className="h-10 w-10">
<AvatarImage src={session.user.image || ''} alt={session.user.name || ''} />
<AvatarFallback>
{session.user.name?.[0] || session.user.email?.[0] || 'U'}
</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{session.user.name}</p>
<p className="text-xs leading-none text-muted-foreground">
{session.user.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{session.user.role === 'ADMIN' && adminNavigation.map((item) => (
<DropdownMenuItem key={item.name} asChild>
<Link href={item.href} className="flex items-center">
<item.icon className="mr-2 h-4 w-4" />
{item.name}
</Link>
</DropdownMenuItem>
))}
{(session.user.role === 'MEMBER' || session.user.role === 'ADMIN') &&
memberNavigation.map((item) => (
<DropdownMenuItem key={item.name} asChild>
<Link href={item.href} className="flex items-center">
<item.icon className="mr-2 h-4 w-4" />
{item.name}
</Link>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => signOut()}>
<LogOut className="mr-2 h-4 w-4" />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<div className="flex items-center space-x-2">
<Button variant="ghost" className="h-10 px-4 py-2 text-base font-medium" asChild>
<Link href="/auth/signin">Sign In</Link>
</Button>
<Button className="h-10 px-4 py-2 text-base font-medium" asChild>
<Link href="/auth/signup">Sign Up</Link>
</Button>
</div>
)}
</div>
</div>
{/* Mobile Menu Button */}
<div className="lg:hidden">
<button
onClick={handleMobileMenuClick}
className="p-4 rounded-lg text-slate-700 hover:bg-slate-100 transition-colors duration-200 touch-manipulation min-w-[48px] min-h-[48px] flex items-center justify-center"
aria-label="Toggle mobile menu"
>
{mobileMenuOpen ? <X className="w-7 h-7" /> : <Menu className="w-7 h-7" />}
</button>
</div>
</div>
</div>
{/* Mobile Menu */}
<AnimatePresence>
{mobileMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="lg:hidden border-t bg-white border-slate-200 shadow-lg relative z-50"
>
<div className="px-4 py-6 space-y-6 max-h-[80vh] overflow-y-auto">
{/* Regular Nav Items */}
{navItems.map(item => (
<Link
key={item.name}
href={item.href}
className="mobile-nav-item block text-lg font-medium text-slate-700 hover:text-emerald-600 transition-colors duration-200 py-3 px-2 rounded-lg hover:bg-slate-50 touch-manipulation"
onClick={handleMobileLinkClick}
>
{item.name}
</Link>
))}
{/* Mobile Mega Menu Items */}
{Object.entries(megaMenus).map(([key, menu]) => (
<div key={key} className="space-y-3">
<h3 className="font-semibold text-slate-800 text-lg sm:text-xl">{menu.title}</h3>
{menu.type === 'categories' ? (
// Products with Categories - Mobile Responsive
<div className="space-y-4">
{menu.categories?.map((category: MenuCategory, idx: number) => {
const IconComponent = category.icon
return (
<div key={idx} className="space-y-2">
<div className="flex items-center space-x-2 text-emerald-600 font-medium text-base sm:text-lg">
<IconComponent className="w-4 h-4 flex-shrink-0" />
<span>{category.name}</span>
</div>
<div className="pl-6 space-y-1">
{category.items.map((item: MenuItem, itemIdx: number) => (
<Link
key={itemIdx}
href={item.href}
className="mobile-nav-item block text-base text-slate-600 hover:text-emerald-600 hover:bg-slate-50 transition-colors duration-200 py-2 px-2 rounded-md touch-manipulation"
onClick={handleMobileLinkClick}
>
<div className="font-medium">{item.name}</div>
<div className="text-xs text-slate-500 mt-0.5">{item.description}</div>
</Link>
))}
</div>
</div>
)
})}
</div>
) : null}
</div>
))}
{/* Mobile CTA */}
<Link href="/contact#quote" onClick={handleMobileLinkClick}>
<Button className="w-full bg-emerald-600 text-white hover:bg-emerald-700 rounded-xl px-6 py-4 mt-6 font-medium text-lg touch-manipulation">
Get a Quote
</Button>
</Link>
</div>
</motion.div>
)}
</AnimatePresence>
</nav>
)
}

View File

@@ -0,0 +1,224 @@
'use client'
import { useState } from 'react'
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Star, ThumbsUp, Flag, MoreHorizontal, Edit, Trash2, Verified } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { formatDistanceToNow } from 'date-fns'
import Image from 'next/image'
import { toast } from 'sonner'
import { useSession } from 'next-auth/react'
interface Review {
id: string
rating: number
title?: string
comment?: string
images: string[]
isVerified: boolean
helpfulVotes: number
createdAt: string
user: {
id: string
name: string
image?: string
}
_count?: {
helpfulVotedBy: number
}
}
interface ReviewCardProps {
review: Review
onEdit?: (review: Review) => void
onDelete?: (reviewId: string) => void
onHelpfulVote?: (reviewId: string) => void
onReport?: (reviewId: string) => void
userHasVoted?: boolean
}
export default function ReviewCard({
review,
onEdit,
onDelete,
onHelpfulVote,
onReport,
userHasVoted = false
}: ReviewCardProps) {
const { data: session } = useSession()
const [isVoting, setIsVoting] = useState(false)
const [hasVoted, setHasVoted] = useState(userHasVoted)
const [voteCount, setVoteCount] = useState(review.helpfulVotes)
const isOwner = session?.user?.id === review.user.id
const handleHelpfulVote = async () => {
if (!session) {
toast.error('Please sign in to vote on reviews')
return
}
setIsVoting(true)
try {
const response = await fetch(`/api/reviews/${review.id}/helpful`, {
method: 'POST'
})
const data = await response.json()
if (response.ok) {
setHasVoted(data.voted)
setVoteCount(prev => data.voted ? prev + 1 : prev - 1)
toast.success(data.message)
onHelpfulVote?.(review.id)
} else {
toast.error(data.error || 'Failed to vote')
}
} catch (error) {
toast.error('Failed to vote on review')
} finally {
setIsVoting(false)
}
}
const handleReport = () => {
if (!session) {
toast.error('Please sign in to report reviews')
return
}
onReport?.(review.id)
}
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 (
<Card className="mb-4">
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-center space-x-3">
<Avatar className="h-10 w-10">
<AvatarImage src={review.user.image} alt={review.user.name} />
<AvatarFallback>
{review.user.name?.split(' ').map(n => n[0]).join('') || 'U'}
</AvatarFallback>
</Avatar>
<div>
<div className="flex items-center space-x-2">
<span className="font-semibold">{review.user.name}</span>
{review.isVerified && (
<Badge variant="outline" className="text-green-600 border-green-600">
<Verified className="h-3 w-3 mr-1" />
Verified Purchase
</Badge>
)}
</div>
<div className="flex items-center space-x-2 text-sm text-gray-600">
<div className="flex items-center space-x-1">
{renderStars(review.rating)}
</div>
<span></span>
<span>{formatDistanceToNow(new Date(review.createdAt), { addSuffix: true })}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
{isOwner && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onEdit?.(review)}>
<Edit className="h-4 w-4 mr-2" />
Edit Review
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onDelete?.(review.id)}
className="text-red-600"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete Review
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
</CardHeader>
<CardContent className="pt-0">
{review.title && (
<h4 className="font-semibold mb-2">{review.title}</h4>
)}
{review.comment && (
<p className="text-gray-700 mb-3 leading-relaxed">{review.comment}</p>
)}
{review.images.length > 0 && (
<div className="flex space-x-2 mb-3 overflow-x-auto">
{review.images.map((image, index) => (
<div key={index} className="relative w-20 h-20 flex-shrink-0">
<Image
src={image}
alt={`Review image ${index + 1}`}
fill
className="object-cover rounded-md"
/>
</div>
))}
</div>
)}
<div className="flex items-center justify-between pt-3 border-t">
<div className="flex items-center space-x-4">
<Button
variant="ghost"
size="sm"
onClick={handleHelpfulVote}
disabled={isVoting}
className={`${hasVoted ? 'text-green-600' : 'text-gray-600'}`}
>
<ThumbsUp className={`h-4 w-4 mr-1 ${hasVoted ? 'fill-current' : ''}`} />
Helpful ({voteCount})
</Button>
</div>
{!isOwner && (
<Button
variant="ghost"
size="sm"
onClick={handleReport}
className="text-gray-600 hover:text-red-600"
>
<Flag className="h-4 w-4 mr-1" />
Report
</Button>
)}
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,300 @@
'use client'
import { useState } from 'react'
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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Star, Upload, X } from 'lucide-react'
import { toast } from 'sonner'
import { useSession } from 'next-auth/react'
import Image from 'next/image'
interface ReviewFormProps {
productId: string
productName: string
onSubmit?: (review: any) => void
onCancel?: () => void
disableImageUpload?: boolean
existingReview?: {
id: string
rating: number
title?: string
comment?: string
images: string[]
}
}
export default function ReviewForm({
productId,
productName,
onSubmit,
onCancel,
disableImageUpload = false,
existingReview
}: ReviewFormProps) {
const { data: session } = useSession()
const [rating, setRating] = useState(existingReview?.rating || 5)
const [title, setTitle] = useState(existingReview?.title || '')
const [comment, setComment] = useState(existingReview?.comment || '')
const [images, setImages] = useState<string[]>(existingReview?.images || [])
const [hoveredStar, setHoveredStar] = useState(0)
const [isSubmitting, setIsSubmitting] = useState(false)
const [uploadingImage, setUploadingImage] = useState(false)
if (!session) {
return (
<Card>
<CardContent className="text-center py-8">
<p className="text-gray-600 mb-4">Please sign in to write a review</p>
<Button>Sign In</Button>
</CardContent>
</Card>
)
}
const handleImageUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
if (images.length >= 5) {
toast.error('Maximum 5 images allowed')
return
}
setUploadingImage(true)
try {
const formData = new FormData()
formData.append('file', file)
formData.append('folder', 'reviews')
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
})
if (response.ok) {
const data = await response.json()
setImages(prev => [...prev, data.url])
toast.success('Image uploaded successfully')
} else {
toast.error('Failed to upload image')
}
} catch (error) {
toast.error('Failed to upload image')
} finally {
setUploadingImage(false)
}
}
const removeImage = (index: number) => {
setImages(prev => prev.filter((_, i) => i !== index))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!rating) {
toast.error('Please select a rating')
return
}
setIsSubmitting(true)
try {
const url = existingReview
? `/api/reviews/${existingReview.id}`
: '/api/reviews'
const method = existingReview ? 'PUT' : 'POST'
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
productId,
rating,
title: title.trim() || undefined,
comment: comment.trim() || undefined,
images
})
})
const data = await response.json()
if (response.ok) {
toast.success(data.message || 'Review submitted successfully')
onSubmit?.(data.review)
// Reset form if creating new review
if (!existingReview) {
setRating(5)
setTitle('')
setComment('')
setImages([])
}
} else {
toast.error(data.error || 'Failed to submit review')
}
} catch (error) {
toast.error('Failed to submit review')
} finally {
setIsSubmitting(false)
}
}
const renderStars = () => {
return Array.from({ length: 5 }, (_, index) => {
const starValue = index + 1
return (
<button
key={index}
type="button"
onClick={() => setRating(starValue)}
onMouseEnter={() => setHoveredStar(starValue)}
onMouseLeave={() => setHoveredStar(0)}
className="focus:outline-none"
>
<Star
className={`h-8 w-8 transition-colors ${
starValue <= (hoveredStar || rating)
? 'text-yellow-400 fill-current'
: 'text-gray-300'
}`}
/>
</button>
)
})
}
return (
<Card>
<CardHeader>
<CardTitle>
{existingReview ? 'Edit Your Review' : 'Write a Review'}
</CardTitle>
<p className="text-sm text-gray-600">for {productName}</p>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Rating */}
<div className="space-y-2">
<Label>Rating *</Label>
<div className="flex items-center space-x-1">
{renderStars()}
<span className="ml-2 text-sm text-gray-600">
{rating}/5 stars
</span>
</div>
</div>
{/* Title */}
<div className="space-y-2">
<Label htmlFor="title">Review Title</Label>
<Input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Summarize your experience"
maxLength={100}
/>
<p className="text-xs text-gray-500">{title.length}/100 characters</p>
</div>
{/* Comment */}
<div className="space-y-2">
<Label htmlFor="comment">Your Review</Label>
<Textarea
id="comment"
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Share your thoughts about this product..."
rows={4}
maxLength={1000}
/>
<p className="text-xs text-gray-500">{comment.length}/1000 characters</p>
</div>
{/* Images */}
{!disableImageUpload && (
<div className="space-y-2">
<Label>Photos (Optional)</Label>
<div className="space-y-3">
{images.length > 0 && (
<div className="grid grid-cols-5 gap-2">
{images.map((image, index) => (
<div key={index} className="relative group">
<Image
src={image}
alt={`Review image ${index + 1}`}
width={80}
height={80}
className="object-cover rounded-md"
/>
<button
type="button"
onClick={() => removeImage(index)}
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
>
<X className="h-3 w-3" />
</button>
</div>
))}
</div>
)}
{images.length < 5 && (
<div>
<label className="cursor-pointer">
<div className="border-2 border-dashed border-gray-300 rounded-md p-4 text-center hover:border-gray-400 transition-colors">
<Upload className="h-6 w-6 mx-auto mb-2 text-gray-400" />
<p className="text-sm text-gray-600">
{uploadingImage ? 'Uploading...' : 'Click to upload photos'}
</p>
<p className="text-xs text-gray-500">
Max 5 images, 5MB each
</p>
</div>
<input
type="file"
accept="image/*"
onChange={handleImageUpload}
disabled={uploadingImage}
className="hidden"
/>
</label>
</div>
)}
</div>
</div>
)}
{/* Submit Buttons */}
<div className="flex items-center space-x-3 pt-4">
<Button
type="submit"
disabled={isSubmitting || !rating}
>
{isSubmitting
? (existingReview ? 'Updating...' : 'Submitting...')
: (existingReview ? 'Update Review' : 'Submit Review')
}
</Button>
{onCancel && (
<Button type="button" variant="outline" onClick={onCancel}>
Cancel
</Button>
)}
</div>
<p className="text-xs text-gray-500">
Your review will be visible after admin approval.
Reviews from verified purchases are marked as verified.
</p>
</form>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,447 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Star, Filter, Plus } from 'lucide-react'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import ReviewCard from './ReviewCard'
import ReviewForm from './ReviewForm'
import { toast } from 'sonner'
import { useSession } from 'next-auth/react'
interface Review {
id: string
rating: number
title?: string
comment?: string
images: string[]
isVerified: boolean
helpfulVotes: number
createdAt: string
userHasVoted?: boolean // Track if current user has voted this review as helpful
user: {
id: string
name: string
image?: string
}
_count?: {
helpfulVotedBy: number
}
}
interface ReviewsListProps {
productId: string
productName: string
averageRating?: number
totalReviews?: number
ratingBreakdown?: { [key: number]: number }
disableImageUpload?: boolean
}
export default function ReviewsList({
productId,
productName,
averageRating = 0,
totalReviews = 0,
ratingBreakdown = {},
disableImageUpload = false
}: ReviewsListProps) {
const { data: session } = useSession()
const [reviews, setReviews] = useState<Review[]>([])
const [loading, setLoading] = useState(true)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [ratingFilter, setRatingFilter] = useState<string>('all')
const [showForm, setShowForm] = useState(false)
const [editingReview, setEditingReview] = useState<Review | null>(null)
const [userReview, setUserReview] = useState<Review | null>(null)
const [allReviews, setAllReviews] = useState<Review[]>([])
const [calculatedStats, setCalculatedStats] = useState({
averageRating: 0,
totalReviews: 0,
ratingBreakdown: {} as { [key: number]: number }
})
const scrollToReviews = () => {
const reviewsSection = document.getElementById('reviews-section')
if (reviewsSection) {
reviewsSection.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
// If there are no reviews, automatically show the form
if (calculatedStats.totalReviews === 0) {
setTimeout(() => setShowForm(true), 500)
}
}
}
const calculateStats = useCallback((allReviewsData: Review[]) => {
const total = allReviewsData.length
if (total === 0) {
setCalculatedStats({
averageRating: 0,
totalReviews: 0,
ratingBreakdown: {}
})
return
}
const sum = allReviewsData.reduce((acc, review) => acc + review.rating, 0)
const avg = sum / total
const breakdown: { [key: number]: number } = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }
allReviewsData.forEach(review => {
breakdown[review.rating] = (breakdown[review.rating] || 0) + 1
})
setCalculatedStats({
averageRating: avg,
totalReviews: total,
ratingBreakdown: breakdown
})
}, [])
const fetchAllReviewsForStats = useCallback(async () => {
try {
const params = new URLSearchParams({
productId,
limit: '1000' // Get all reviews for stats
})
const response = await fetch(`/api/reviews?${params}`)
const data = await response.json()
if (response.ok) {
setAllReviews(data.reviews)
calculateStats(data.reviews)
}
} catch (error) {
console.error('Failed to fetch all reviews for stats:', error)
}
}, [productId, calculateStats])
const fetchReviews = useCallback(async (resetList = false) => {
try {
setLoading(true)
const params = new URLSearchParams({
productId,
page: resetList ? '1' : page.toString(),
limit: '10'
})
if (ratingFilter !== 'all') {
params.append('rating', ratingFilter)
}
const response = await fetch(`/api/reviews?${params}`)
const data = await response.json()
if (response.ok) {
if (resetList) {
setReviews(data.reviews)
setPage(1)
} else {
setReviews(prev => [...prev, ...data.reviews])
}
setHasMore(data.pagination.page < data.pagination.pages)
// Check if current user has a review
if (session?.user) {
const userReviewInList = data.reviews.find(
(review: Review) => review.user.id === session.user.id
)
setUserReview(userReviewInList || null)
}
// If this is the first page, also fetch all reviews for stats
if (resetList) {
fetchAllReviewsForStats()
}
} else {
toast.error('Failed to load reviews')
}
} catch (error) {
toast.error('Failed to load reviews')
} finally {
setLoading(false)
}
}, [productId, page, ratingFilter, session?.user, fetchAllReviewsForStats])
useEffect(() => {
fetchReviews(true)
}, [fetchReviews])
const loadMore = () => {
setPage(prev => prev + 1)
fetchReviews()
}
const handleReviewSubmit = (newReview: Review) => {
setShowForm(false)
setEditingReview(null)
setUserReview(newReview)
fetchReviews(true) // Refresh the list
}
const handleEdit = (review: Review) => {
setEditingReview(review)
setShowForm(true)
}
const handleDelete = async (reviewId: string) => {
if (!confirm('Are you sure you want to delete this review?')) return
try {
const response = await fetch(`/api/reviews/${reviewId}`, {
method: 'DELETE'
})
if (response.ok) {
toast.success('Review deleted successfully')
setReviews(prev => prev.filter(review => review.id !== reviewId))
setUserReview(null)
} else {
const data = await response.json()
toast.error(data.error || 'Failed to delete review')
}
} catch (error) {
toast.error('Failed to delete review')
}
}
const handleHelpfulVote = async (reviewId: string) => {
if (!session?.user) {
toast.error('Please sign in to vote')
return
}
try {
const response = await fetch(`/api/reviews/${reviewId}/helpful`, {
method: 'POST'
})
if (response.ok) {
const data = await response.json()
// Update the review in the list with new vote status and count
setReviews(prev => prev.map(review =>
review.id === reviewId
? {
...review,
userHasVoted: data.hasVoted,
helpfulVotes: data.totalVotes,
_count: {
...review._count,
helpfulVotedBy: data.totalVotes
}
}
: review
))
toast.success(data.hasVoted ? 'Marked as helpful' : 'Removed helpful vote')
} else {
const data = await response.json()
toast.error(data.error || 'Failed to vote')
}
} catch (error) {
toast.error('Failed to vote')
}
}
const renderRatingBreakdown = () => {
if (!calculatedStats.totalReviews) return null
return (
<div className="space-y-2">
{[5, 4, 3, 2, 1].map(rating => {
const count = calculatedStats.ratingBreakdown[rating] || 0
const percentage = calculatedStats.totalReviews > 0 ? (count / calculatedStats.totalReviews) * 100 : 0
return (
<div key={rating} className="flex items-center space-x-2 text-sm">
<div className="flex items-center space-x-1 w-16">
<span>{rating}</span>
<Star className="h-3 w-3 text-yellow-400 fill-current" />
</div>
<div className="flex-1 bg-gray-200 rounded-full h-2">
<div
className="bg-yellow-400 h-2 rounded-full transition-all"
style={{ width: `${percentage}%` }}
/>
</div>
<span className="w-8 text-right">{count}</span>
</div>
)
})}
</div>
)
}
return (
<div id="reviews-section" className="space-y-6">
{/* Reviews Summary - Only show if there are reviews */}
{calculatedStats.totalReviews > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Customer Reviews</span>
{session && !userReview && !showForm && (
<Button onClick={scrollToReviews}>
<Plus className="h-4 w-4 mr-2" />
Write Review
</Button>
)}
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Overall Rating */}
<div className="text-center">
<div className="text-4xl font-bold mb-2">{calculatedStats.averageRating.toFixed(1)}</div>
<div className="flex items-center justify-center mb-2">
{Array.from({ length: 5 }, (_, index) => (
<Star
key={index}
className={`h-5 w-5 ${
index < Math.round(calculatedStats.averageRating)
? 'text-yellow-400 fill-current'
: 'text-gray-300'
}`}
/>
))}
</div>
<p className="text-gray-600">{calculatedStats.totalReviews} reviews</p>
</div>
{/* Rating Breakdown */}
<div>
<h4 className="font-semibold mb-3">Rating Breakdown</h4>
{renderRatingBreakdown()}
</div>
</div>
</CardContent>
</Card>
)}
{/* No Reviews State - Show when there are no reviews */}
{calculatedStats.totalReviews === 0 && !loading && (
<Card>
<CardContent className="text-center py-12">
<div className="space-y-4">
<div className="flex items-center justify-center">
{Array.from({ length: 5 }, (_, index) => (
<Star
key={index}
className="h-8 w-8 text-gray-300"
/>
))}
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">No reviews yet</h3>
<p className="text-gray-600 mb-4">Be the first to review this product</p>
{session ? (
<Button onClick={() => setShowForm(true)} className="bg-emerald-600 hover:bg-emerald-700">
<Plus className="h-4 w-4 mr-2" />
Write First Review
</Button>
) : (
<p className="text-sm text-gray-500">Sign in to write a review</p>
)}
</div>
</div>
</CardContent>
</Card>
)}
{/* Review Form */}
{showForm && (
<ReviewForm
productId={productId}
productName={productName}
existingReview={editingReview || undefined}
disableImageUpload={disableImageUpload}
onSubmit={handleReviewSubmit}
onCancel={() => {
setShowForm(false)
setEditingReview(null)
}}
/>
)}
{/* Filters and Reviews List */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">All Reviews ({calculatedStats.totalReviews})</h3>
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-gray-500" />
<Select value={ratingFilter} onValueChange={setRatingFilter}>
<SelectTrigger className="w-32">
<SelectValue placeholder="Filter" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Ratings</SelectItem>
<SelectItem value="5">5 Stars</SelectItem>
<SelectItem value="4">4 Stars</SelectItem>
<SelectItem value="3">3 Stars</SelectItem>
<SelectItem value="2">2 Stars</SelectItem>
<SelectItem value="1">1 Star</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Reviews */}
{loading && reviews.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-600">Loading reviews...</p>
</div>
) : reviews.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-600 mb-4">No reviews yet</p>
{session && !userReview && (
<Button onClick={() => setShowForm(true)}>
Be the first to review
</Button>
)}
</div>
) : (
<div>
{reviews.map((review) => (
<ReviewCard
key={review.id}
review={review}
onEdit={handleEdit}
onDelete={handleDelete}
onHelpfulVote={handleHelpfulVote}
userHasVoted={review.userHasVoted || false}
/>
))}
{hasMore && (
<div className="text-center pt-4">
<Button
variant="outline"
onClick={loadMore}
disabled={loading}
>
{loading ? 'Loading...' : 'Load More Reviews'}
</Button>
</div>
)}
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,184 @@
'use client'
import { Badge } from '@/components/ui/badge'
import { Wheat, Shield, Award, ArrowRight, Star, CheckCircle, Users, TrendingUp } from 'lucide-react'
import { Button } from '../ui/button'
import Image from 'next/image'
import Link from 'next/link'
import { motion } from 'framer-motion'
export default function AboutSection() {
const stats = [
{ number: "100%", label: "Customer Satisfaction", icon: Award, color: "from-amber-500 to-yellow-600" },
{ number: "10K+", label: "Rice Loving Families", icon: Users, color: "from-emerald-500 to-green-600" },
{ number: "8000+", label: "Tons Processed Monthly", icon: TrendingUp, color: "from-orange-500 to-red-600" },
{ number: "99%", label: "Pure & Natural Rice", icon: TrendingUp, color: "from-yellow-500 to-amber-600" }
]
const features = [
"Direct sourcing from Punjab & Haryana rice belt",
"Traditional aging process for Basmati rice",
"Steam processing for premium Sella varieties",
"Farm-to-table traceability and quality assurance"
]
return (
<section className="relative py-12 md:py-32 overflow-hidden">
{/* Modern gradient background with rice-inspired colors */}
<div className="absolute inset-0 bg-gradient-to-br from-amber-50 via-yellow-50 to-orange-50/30"></div>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_20%,rgba(245,158,11,0.05),transparent_50%)]"></div>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_70%_80%,rgba(251,191,36,0.05),transparent_50%)]"></div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Section header with modern typography */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-8 md:mb-20"
>
<div className="inline-flex items-center gap-2 bg-amber-100/50 backdrop-blur-sm px-4 py-2 rounded-full mb-6">
<Wheat className="w-4 h-4 text-amber-600" />
<span className="text-amber-700 font-medium text-sm">Our Rice Heritage</span>
</div>
<h2 className="text-3xl md:text-4xl font-bold text-slate-900 mb-4">
From Golden Fields to {''}
<span className="text-transparent bg-gradient-to-r from-amber-600 via-yellow-500 to-orange-500 bg-clip-text">
Your Kitchen
</span>
</h2>
<p className="text-lg text-slate-600 max-w-3xl mx-auto leading-relaxed">
Bringing you the finest rice varieties from the fertile plains of Northern India, aged to perfection and processed with traditional care
</p>
</motion.div>
{/* Main content grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 sm:gap-12 lg:gap-20 items-center mb-16 sm:mb-20">
{/* Visual side - First on mobile, second on desktop */}
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.3 }}
className="relative order-1 lg:order-2"
>
{/* Main image with modern styling - 16:9 on mobile, original aspect on desktop */}
<div className="relative rounded-3xl overflow-hidden shadow-2xl aspect-video lg:aspect-auto">
<div className="absolute inset-0 bg-gradient-to-br from-emerald-600/20 via-transparent to-orange-600/20 z-10"></div>
<Image
src="/farmer.png"
alt="Premium food processing facility"
width={600}
height={500}
className="w-full lg:h-[500px] object-cover"
priority
/>
{/* Floating quality badge */}
<div className="absolute top-4 left-4 sm:top-6 sm:left-6 bg-white/95 backdrop-blur-sm rounded-2xl px-3 py-2 sm:px-4 sm:py-3 shadow-lg z-20">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
<span className="font-semibold text-slate-900 text-xs sm:text-sm">Premium Certified</span>
</div>
</div>
</div>
{/* Decorative elements */}
<div className="absolute -top-6 -right-6 w-24 h-24 bg-gradient-to-br from-orange-400 to-orange-500 rounded-full opacity-20 blur-xl"></div>
<div className="absolute -bottom-4 -left-4 w-20 h-20 bg-gradient-to-br from-emerald-400 to-emerald-500 rounded-full opacity-20 blur-xl"></div>
</motion.div>
{/* Content side - Second on mobile, first on desktop */}
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
className="space-y-6 sm:space-y-8 order-2 lg:order-1"
>
{/* Story */}
<div className="space-y-4 sm:space-y-6">
<h3 className="text-xl sm:text-2xl md:text-3xl font-bold text-slate-900 leading-tight">
Two Decades of
<span className="text-emerald-600"> Trusted Excellence</span>
</h3>
<div className="space-y-3 sm:space-y-4 text-base sm:text-lg leading-relaxed text-slate-600">
<p>
Padmaaja Rasooi has revolutionized food processing with unwavering commitment to quality and authenticity.
</p>
<p>
We&apos;ve built lasting relationships with farmers, ensuring every product reflects our dedication to
excellence and the highest standards of food safety.
</p>
</div>
</div>
{/* Feature list */}
<div className="space-y-3 sm:space-y-4">
{features.map((feature, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: 0.3 + index * 0.1 }}
className="flex items-center gap-3"
>
<div className="w-4 h-4 sm:w-5 sm:h-5 rounded-full bg-gradient-to-br from-emerald-500 to-emerald-600 flex items-center justify-center flex-shrink-0">
<CheckCircle className="w-2.5 h-2.5 sm:w-3 sm:h-3 text-white" />
</div>
<span className="text-slate-700 font-medium text-sm sm:text-base">{feature}</span>
</motion.div>
))}
{/* CTA */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.6 }}
className="pt-3"
>
<Button asChild size="lg" className="w-full sm:w-auto bg-gradient-to-r from-emerald-600 to-emerald-700 hover:from-emerald-700 hover:to-emerald-800 text-white px-6 sm:px-8 py-3 sm:py-4 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 group">
<Link href="/about" className="inline-flex items-center justify-center text-sm sm:text-lg">
Discover Our Story
<ArrowRight className="ml-2 h-4 w-4 sm:h-5 sm:w-5 group-hover:translate-x-1 transition-transform" />
</Link>
</Button>
</motion.div>
</div>
</motion.div>
</div>
{/* Stats section */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}
className="grid grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 lg:gap-8"
>
{stats.map((stat, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: 0.5 + index * 0.1 }}
className="relative group"
>
<div className="bg-white/80 backdrop-blur-sm rounded-xl sm:rounded-2xl p-4 sm:p-6 lg:p-8 shadow-lg hover:shadow-xl transition-all duration-300 border border-white/50 group-hover:scale-105">
<div className={`w-8 h-8 sm:w-10 sm:h-10 lg:w-12 lg:h-12 bg-gradient-to-br ${stat.color} rounded-lg sm:rounded-xl flex items-center justify-center mb-3 sm:mb-4 group-hover:scale-110 transition-transform`}>
<stat.icon className="w-4 h-4 sm:w-5 sm:h-5 lg:w-6 lg:h-6 text-white" />
</div>
<div className="text-xl sm:text-2xl lg:text-3xl xl:text-4xl font-bold text-slate-900 mb-1 sm:mb-2">{stat.number}</div>
<div className="text-slate-600 font-medium text-xs sm:text-sm lg:text-base leading-tight">{stat.label}</div>
</div>
</motion.div>
))}
</motion.div>
</div>
</section>
)
}

View File

@@ -0,0 +1,115 @@
'use client'
import Image from 'next/image'
import { motion } from 'framer-motion'
export default function CertificationsSection() {
const certifications = [
{
id: 1,
name: "FSSAI",
image: "/certifications/Frame-1000003749.png",
},
{
id: 2,
name: "ISO",
image: "/certifications/Frame-1000003750.png",
},
{
id: 3,
name: "GMP",
image: "/certifications/Frame-1000003751.png",
},
{
id: 4,
name: "HACCP",
image: "/certifications/image-371-1.png",
},
{
id: 5,
name: "FDA",
image: "/certifications/image-375-1.png",
},
{
id: 6,
name: "BRC",
image: "/certifications/image-377-2.png",
},
{
id: 7,
name: "Quality",
image: "/certifications/image-92.png",
}
]
return (
<section className="relative py-12 md:py-16 overflow-hidden bg-white">
<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: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-8 md:mb-12"
>
<h2 className="text-2xl md:text-3xl font-bold text-slate-900 mb-2">
<span className="text-transparent bg-gradient-to-r from-red-600 via-red-500 to-orange-500 bg-clip-text">
Certified By
</span>
</h2>
<p className="text-slate-600 max-w-2xl mx-auto">
Our commitment to quality is validated by prestigious international certifications
</p>
</motion.div>
{/* Certifications Row */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
className="flex flex-wrap justify-center items-center gap-6 md:gap-8 lg:gap-12"
>
{certifications.map((cert, index) => (
<motion.div
key={cert.id}
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: index * 0.1 }}
whileHover={{ scale: 1.05 }}
className="group cursor-pointer"
>
<div className="relative w-20 h-20 md:w-24 md:h-24 lg:w-28 lg:h-28 bg-white rounded-full shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 group-hover:border-gray-200 flex items-center justify-center p-3">
<Image
src={cert.image}
alt={cert.name}
fill
className="object-contain p-2 group-hover:scale-110 transition-transform duration-300"
sizes="(max-width: 768px) 80px, (max-width: 1024px) 96px, 112px"
/>
</div>
<p className="text-center text-xs md:text-sm font-medium text-gray-600 mt-2 group-hover:text-gray-900 transition-colors">
{cert.name}
</p>
</motion.div>
))}
</motion.div>
{/* Bottom Text */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.5 }}
className="text-center mt-8 md:mt-12"
>
<p className="text-sm text-gray-500 max-w-xl mx-auto">
Trusted by industry leaders worldwide for our commitment to quality, safety, and excellence in food production.
</p>
</motion.div>
</div>
</section>
)
}

View File

@@ -0,0 +1,36 @@
'use client'
import { useState } from 'react'
import { AnimatePresence } from 'framer-motion'
import PageLoader from '@/components/ui/page-loader'
interface ClientPageWrapperProps {
children: React.ReactNode
}
export default function ClientPageWrapper({ children }: ClientPageWrapperProps) {
const [isLoading, setIsLoading] = useState(true)
const handleLoadingComplete = () => {
setIsLoading(false)
}
return (
<>
<AnimatePresence mode="wait">
{isLoading && (
<PageLoader
onLoadingComplete={handleLoadingComplete}
duration={2000}
/>
)}
</AnimatePresence>
{!isLoading && (
<div className="animate-in fade-in duration-500">
{children}
</div>
)}
</>
)
}

View File

@@ -0,0 +1,349 @@
'use client'
import { useState, useEffect, useMemo } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { motion, AnimatePresence } from 'framer-motion'
import { ChevronLeft, ChevronRight, Wheat } from 'lucide-react'
import { Button } from '@/components/ui/button'
interface Banner {
id: number
title: string
subtitle: string
description: string
image: string
cta: string
ctaLink?: string
bgColor?: string
}
interface FeatureCard {
icon: any
title: string
description: string
link: string
color: string
}
export default function HeroSection() {
const [currentSlide, setCurrentSlide] = useState(0)
const [imageLoading, setImageLoading] = useState(true)
const [touchStart, setTouchStart] = useState<number | null>(null)
const [touchEnd, setTouchEnd] = useState<number | null>(null)
const banners: Banner[] = useMemo(() => [
{
id: 1,
title: "Premium Aged Basmati 1121",
subtitle: "Extra Long Grain • 2+ Years Aged",
description: "Authentic Basmati rice with exceptional length, distinctive aroma, and fluffy texture from the foothills of Himalayas",
image: "/images/rice-hero-slider.jpg",
cta: "Shop Premium Rice",
ctaLink: "/products?category=rice",
bgColor: "from-amber-600 via-yellow-600 to-orange-700"
},
{
id: 3,
title: "Kashmina Brand Collection",
subtitle: "Farm Fresh • Direct from Fields",
description: "Premium rice varieties sourced directly from trusted farmers, processed with care for exceptional quality",
image: "https://4m5m4tx28rtva30c.public.blob.vercel-storage.com/media/2025-09-07/banner-slider-1",
cta: "Shop Kashmina",
ctaLink: "/products?brand=kashmina",
bgColor: "from-emerald-600 via-green-600 to-yellow-600"
},
// {
// id: 4,
// title: "Wholesale Rice Supplies",
// subtitle: "Bulk Orders • Competitive Rates",
// description: "Reliable wholesale rice supplier offering bulk quantities of premium Basmati and Sella rice for businesses and retailers",
// image: "https://4m5m4tx28rtva30c.public.blob.vercel-storage.com/women-carrying-rice",
// cta: "Shop Wholesale",
// ctaLink: "/wholesaler",
// bgColor: "from-orange-600 via-red-600 to-amber-600"
// },
{
id: 5,
title: "Indian Frmer Plowing",
subtitle: "Traditional Farming • Heritage Grain",
description: "Experience the rich heritage of rice cultivation with traditional farming methods passed down through generations.",
image: "https://4m5m4tx28rtva30c.public.blob.vercel-storage.com/media/2025-09-07/indian-farmer-plowing",
cta: "Learn More",
}
], [])
// Debug: Log current slide image
useEffect(() => {
console.log('Current slide image:', banners[currentSlide]?.image);
}, [currentSlide, banners]);
const featuredCategories = [
{
title: "Kashmina Sella Premium",
subtitle: "Extra long grain",
link: "/products?brand=basmati-1121",
bgColor: "from-amber-500 to-yellow-600"
},
{
title: "Kashmina Gold Premium",
subtitle: "Steam processed",
link: "/products?category=sella",
bgColor: "from-yellow-500 to-amber-600"
},
{
title: "Kashmina Steam Premium",
subtitle: "Traditional quality",
link: "/products?brand=kashmina",
bgColor: "from-emerald-500 to-green-600"
},
{
title: "Wholesale",
subtitle: "Wholesale rates",
link: "/wholesaler",
bgColor: "from-orange-500 to-red-600"
}
]
useEffect(() => {
const timer = setInterval(() => {
setCurrentSlide((prev) => (prev + 1) % banners.length)
}, 5000) // Auto-slide every 5 seconds
return () => clearInterval(timer)
}, [banners.length])
// Reset loading state when slide changes
useEffect(() => {
setImageLoading(true)
}, [currentSlide])
const nextSlide = () => {
setCurrentSlide((prev) => (prev + 1) % banners.length)
}
const prevSlide = () => {
setCurrentSlide((prev) => (prev - 1 + banners.length) % banners.length)
}
const goToSlide = (index: number) => {
setCurrentSlide(index)
}
// Touch handlers for mobile swipe
const minSwipeDistance = 50
const onTouchStart = (e: React.TouchEvent) => {
setTouchEnd(null) // otherwise the swipe is fired even with usual touch events
setTouchStart(e.targetTouches[0].clientX)
}
const onTouchMove = (e: React.TouchEvent) => {
setTouchEnd(e.targetTouches[0].clientX)
}
const onTouchEnd = () => {
if (!touchStart || !touchEnd) return
const distance = touchStart - touchEnd
const isLeftSwipe = distance > minSwipeDistance
const isRightSwipe = distance < -minSwipeDistance
if (isLeftSwipe) {
nextSlide()
} else if (isRightSwipe) {
prevSlide()
}
}
return (
<div className="relative bg-gradient-to-br from-amber-50/50 via-yellow-50/30 to-orange-50/20">
{/* Amazon-style Hero Layout */}
<div className="max-w-7xl mx-auto px-3 sm:px-4 md:px-6 lg:px-8 py-4 lg:py-12">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-3 sm:gap-4 md:gap-5 lg:gap-6">
{/* Main Banner Slider - Left Side (3/4 width) - Better mobile aspect ratio */}
<div
className="lg:col-span-3 relative overflow-hidden rounded-lg shadow-lg aspect-[16/10] md:aspect-video isolate"
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
>
{/* Skeleton Loader for CLS prevention */}
<div className="absolute inset-0 bg-gradient-to-br from-gray-200 to-gray-300 animate-pulse z-0" />
<AnimatePresence mode="wait">
<motion.div
key={currentSlide}
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -30 }}
transition={{ duration: 0.5, ease: "easeInOut" }}
className="absolute inset-0 isolate"
>
{/* Background Color Fallback */}
<div className={`absolute inset-0 bg-gradient-to-br ${banners[currentSlide].bgColor ?? 'from-amber-600 via-yellow-600 to-orange-700'} z-0`}></div>
<Image
src={banners[currentSlide].image}
alt={banners[currentSlide].title}
fill
className="object-cover z-10"
priority={currentSlide === 0} // Only prioritize first image
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 75vw"
quality={85} // Optimize quality vs file size
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
onLoadStart={() => setImageLoading(true)}
onError={(e) => {
console.error('Image failed to load:', banners[currentSlide].image);
setImageLoading(false);
// Keep the gradient background visible on error
const target = e.target as HTMLImageElement;
target.style.display = 'none';
}}
onLoad={() => {
console.log('Image loaded successfully:', banners[currentSlide].image);
setImageLoading(false);
}}
/>
{/* Loading overlay */}
{imageLoading && (
<div className="absolute inset-0 z-15 flex items-center justify-center">
<div className="animate-pulse">
<div className="w-8 h-8 bg-white/30 rounded-full animate-bounce"></div>
</div>
</div>
)}
{/* Content positioned in center-left with enhanced visibility */}
<div className="absolute inset-0 z-20 flex items-center">
<div className="max-w-lg px-4 sm:px-6 md:px-8 lg:px-10 xl:px-12 w-full">
{/* Semi-transparent background for text */}
{/* <div className="bg-black/40 backdrop-blur-sm rounded-xl md:rounded-2xl p-4 sm:p-5 md:p-6 lg:p-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="space-y-2 sm:space-y-3 md:space-y-4"
>
<p className="text-yellow-300 font-semibold text-sm sm:text-base md:text-lg drop-shadow-lg">
{banners[currentSlide].subtitle}
</p>
<h1 className="text-lg sm:text-xl md:text-2xl lg:text-3xl xl:text-4xl 2xl:text-5xl font-bold text-white leading-tight drop-shadow-2xl">
{banners[currentSlide].title}
</h1>
<p className="text-white text-sm sm:text-base md:text-lg leading-relaxed max-w-xs sm:max-w-sm md:max-w-md drop-shadow-lg">
{banners[currentSlide].description}
</p>
<div className="pt-3 sm:pt-4">
<Button asChild size="default" className="bg-white text-gray-900 hover:bg-gray-100 text-sm sm:text-base font-medium px-5 sm:px-6 md:px-8 py-2.5 sm:py-3 rounded-lg shadow-lg">
<Link href={banners[currentSlide].ctaLink ?? '/'}>
{banners[currentSlide].cta}
</Link>
</Button>
</div>
</motion.div>
</div> */}
</div>
</div>
</motion.div>
</AnimatePresence>
{/* Subtle Navigation Arrows - Better mobile visibility */}
<button
onClick={prevSlide}
className="absolute left-2 top-1/2 -translate-y-1/2 z-30 bg-white/30 hover:bg-white/40 text-white p-2 rounded-full transition-all duration-200 backdrop-blur-sm"
>
<ChevronLeft className="w-4 h-4 sm:w-5 sm:h-5" />
</button>
<button
onClick={nextSlide}
className="absolute right-2 top-1/2 -translate-y-1/2 z-30 bg-white/30 hover:bg-white/40 text-white p-2 rounded-full transition-all duration-200 backdrop-blur-sm"
>
<ChevronRight className="w-4 h-4 sm:w-5 sm:h-5" />
</button>
{/* Slide Indicators - Fixed positioning with background */}
<div className="hidden sm:flex absolute bottom-4 left-1/2 -translate-x-1/2 z-40 space-x-1.5 bg-black/20 backdrop-blur-sm px-2.5 py-1.5 rounded-full">
{banners.map((_, index) => (
<button
key={index}
onClick={() => goToSlide(index)}
className={`!w-1.5 !h-1.5 sm:w-2 !sm:h-2 !md:w-2.5 !md:h-2.5 rounded-full transition-all duration-200 ${
index === currentSlide
? 'bg-white shadow-lg'
: 'bg-white/60 hover:bg-white/80'
}`}
/>
))}
</div>
</div>
{/* Modern Category Cards - Right Side (1/4 width) - Ultra compact mobile */}
<div className="lg:col-span-1 aspect-[4/3] sm:aspect-[16/10] md:aspect-video lg:aspect-auto">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-1 gap-1.5 sm:gap-2 lg:gap-3 h-full">
{featuredCategories.slice(0, 4).map((category, index) => (
<motion.div
key={category.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: index * 0.1 }}
className="min-h-[50px] sm:min-h-[70px] lg:h-full lg:min-h-0"
>
<Link href={category.link}>
<div className="group h-full relative bg-white rounded-md border border-gray-200 hover:border-emerald-300 shadow-sm hover:shadow-md transition-all duration-300 cursor-pointer overflow-hidden">
{/* Gradient Background Accent */}
<div className={`absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r ${category.bgColor}`}></div>
{/* Content - Ultra Compact Mobile Layout */}
<div className="relative px-2 py-1.5 sm:px-3 sm:py-2 lg:p-4 h-full flex items-center gap-2 sm:gap-3">
{/* Icon Container - Very small on mobile */}
<div className={`w-6 h-6 sm:w-8 sm:h-8 lg:w-10 lg:h-10 rounded-md bg-gradient-to-br ${category.bgColor} flex items-center justify-center group-hover:scale-105 transition-all duration-300 shadow-sm flex-shrink-0`}>
<Wheat className="w-3 h-3 sm:w-4 sm:h-4 lg:w-5 lg:h-5 text-white" />
</div>
{/* Typography - Ultra compact */}
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-xs sm:text-sm lg:text-base text-gray-900 group-hover:text-emerald-600 transition-colors leading-tight mb-0.5">
{category.title}
</h3>
<p className="text-[10px] sm:text-xs lg:text-sm text-gray-600 group-hover:text-gray-700 transition-colors">
{category.subtitle}
</p>
</div>
{/* Arrow Icon - Hidden on mobile */}
<div className="hidden lg:block opacity-0 group-hover:opacity-100 transition-all duration-300 flex-shrink-0">
<ChevronRight className="w-4 h-4 text-gray-400 group-hover:text-emerald-600" />
</div>
</div>
</div>
</Link>
</motion.div>
))}
</div>
</div>
</div>
</div>
{/* Secondary Strip - Repositioned and Mobile Optimized */}
{/* <div className="bg-gradient-to-r from-amber-600 via-yellow-600 to-orange-600 text-white py-4 sm:py-5 md:py-6 mt-4 sm:mt-6">
<div className="max-w-7xl mx-auto px-3 sm:px-4 md:px-6 lg:px-8">
<div className="flex flex-col items-center justify-center text-center gap-3 sm:gap-4">
<div className="flex-1 max-w-4xl">
<h3 className="text-sm sm:text-base md:text-lg font-semibold mb-1 sm:mb-2">🌾 Premium Rice • Direct from Farms • Free Shipping ₹500+</h3>
<p className="text-yellow-100 text-xs sm:text-sm md:text-base">Aged Basmati & Golden Sella rice delivered fresh to your kitchen</p>
</div>
<Link href="/products">
<Button variant="outline" size="sm" className="bg-white text-amber-700 hover:bg-yellow-50 border-white text-xs sm:text-sm font-medium px-4 sm:px-6 py-2 sm:py-2.5 rounded-lg shadow-lg hover:shadow-xl transition-all duration-300">
Explore Rice Varieties
</Button>
</Link>
</div>
</div>
</div> */}
</div>
)
}

View File

@@ -0,0 +1,238 @@
'use client'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Star, Award, Truck, ShieldCheck, ArrowRight, Sparkles, Wheat, Heart } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { motion } from 'framer-motion'
export default function KashminaSection() {
return (
<section className="relative py-16 lg:py-24 overflow-hidden">
{/* Enhanced Background */}
<div className="absolute inset-0 bg-gradient-to-br from-amber-50 via-yellow-50 to-orange-50"></div>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_20%,rgba(245,158,11,0.08),transparent_50%)]"></div>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_70%_80%,rgba(251,191,36,0.08),transparent_50%)]"></div>
{/* Animated Background Elements */}
<div className="absolute top-20 left-10 w-32 h-32 bg-amber-200/20 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute bottom-20 right-10 w-40 h-40 bg-yellow-200/20 rounded-full blur-3xl animate-pulse delay-1000"></div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Enhanced Section Header */}
<div className="text-center mb-16">
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-amber-100 to-yellow-100 backdrop-blur-sm px-6 py-3 rounded-full mb-6 border border-amber-200/50 shadow-lg">
<Sparkles className="w-5 h-5 text-amber-600 animate-pulse" />
<span className="text-amber-700 font-semibold text-sm tracking-wide">PREMIUM RICE BRAND</span>
<Sparkles className="w-5 h-5 text-orange-600 animate-pulse delay-500" />
</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 mb-6 leading-tight">
<span className="text-transparent bg-gradient-to-r from-amber-600 via-yellow-600 to-orange-600 bg-clip-text bg-300% animate-gradient">
Kashmina
</span>
<br />
<span className="text-gray-800 text-3xl md:text-4xl lg:text-5xl font-medium">Premium Basmati Rice</span>
</h2>
<p className="text-xl md:text-2xl text-gray-600 max-w-4xl mx-auto leading-relaxed font-light">
Authentic aged Basmati rice with <span className="text-amber-600 font-medium">extraordinary length, exquisite aroma, and royal taste</span>
</p>
</div>
{/* Enhanced Features Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-6xl mx-auto mb-20">
{/* Premium Quality */}
<div className="group cursor-pointer">
<div className="relative bg-white/80 backdrop-blur-sm rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-500 p-8 text-center border border-white/50 group-hover:border-emerald-300/50 group-hover:-translate-y-2 group-hover:bg-white/90 overflow-hidden">
{/* Background decoration */}
<div className="absolute top-0 right-0 w-20 h-20 bg-gradient-to-br from-emerald-100 to-emerald-200 rounded-full -translate-y-10 translate-x-10 opacity-50 group-hover:scale-150 transition-transform duration-700"></div>
<div className="relative z-10">
<div className="w-20 h-20 bg-gradient-to-br from-emerald-400 to-emerald-600 rounded-2xl flex items-center justify-center mx-auto mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-500 shadow-lg shadow-emerald-200">
<Wheat className="w-10 h-10 text-white" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3 group-hover:text-emerald-600 transition-colors duration-300">Authentic Grains</h3>
<p className="text-gray-600 text-sm leading-relaxed mb-4">
Treasured grain with distinct aroma and great nutty taste
</p>
<div className="inline-block bg-gradient-to-r from-emerald-100 to-emerald-200 text-emerald-700 px-4 py-2 rounded-full text-xs font-bold tracking-wide">
AUTHENTIC QUALITY
</div>
</div>
</div>
</div>
{/* Export Quality */}
<div className="group cursor-pointer">
<div className="relative bg-white/80 backdrop-blur-sm rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-500 p-8 text-center border border-white/50 group-hover:border-blue-300/50 group-hover:-translate-y-2 group-hover:bg-white/90 overflow-hidden">
{/* Background decoration */}
<div className="absolute top-0 right-0 w-20 h-20 bg-gradient-to-br from-blue-100 to-blue-200 rounded-full -translate-y-10 translate-x-10 opacity-50 group-hover:scale-150 transition-transform duration-700"></div>
<div className="relative z-10">
<div className="w-20 h-20 bg-gradient-to-br from-blue-400 to-blue-600 rounded-2xl flex items-center justify-center mx-auto mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-500 shadow-lg shadow-blue-200">
<Award className="w-10 h-10 text-white" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3 group-hover:text-blue-600 transition-colors duration-300">Export Grade</h3>
<p className="text-gray-600 text-sm leading-relaxed mb-4">
International quality standards for global markets
</p>
<div className="inline-block bg-gradient-to-r from-blue-100 to-blue-200 text-blue-700 px-4 py-2 rounded-full text-xs font-bold tracking-wide">
CERTIFIED
</div>
</div>
</div>
</div>
{/* Quality Tested */}
<div className="group cursor-pointer">
<div className="relative bg-white/80 backdrop-blur-sm rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-500 p-8 text-center border border-white/50 group-hover:border-purple-300/50 group-hover:-translate-y-2 group-hover:bg-white/90 overflow-hidden">
{/* Background decoration */}
<div className="absolute top-0 right-0 w-20 h-20 bg-gradient-to-br from-purple-100 to-purple-200 rounded-full -translate-y-10 translate-x-10 opacity-50 group-hover:scale-150 transition-transform duration-700"></div>
<div className="relative z-10">
<div className="w-20 h-20 bg-gradient-to-br from-purple-400 to-purple-600 rounded-2xl flex items-center justify-center mx-auto mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-500 shadow-lg shadow-purple-200">
<ShieldCheck className="w-10 h-10 text-white" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3 group-hover:text-purple-600 transition-colors duration-300">Lab Tested</h3>
<p className="text-gray-600 text-sm leading-relaxed mb-4">
Rigorously tested for purity and quality assurance
</p>
<div className="inline-block bg-gradient-to-r from-purple-100 to-purple-200 text-purple-700 px-4 py-2 rounded-full text-xs font-bold tracking-wide">
100% PURE
</div>
</div>
</div>
</div>
{/* Fresh Packaging */}
<div className="group cursor-pointer">
<div className="relative bg-white/80 backdrop-blur-sm rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-500 p-8 text-center border border-white/50 group-hover:border-orange-300/50 group-hover:-translate-y-2 group-hover:bg-white/90 overflow-hidden">
{/* Background decoration */}
<div className="absolute top-0 right-0 w-20 h-20 bg-gradient-to-br from-orange-100 to-orange-200 rounded-full -translate-y-10 translate-x-10 opacity-50 group-hover:scale-150 transition-transform duration-700"></div>
<div className="relative z-10">
<div className="w-20 h-20 bg-gradient-to-br from-orange-400 to-orange-600 rounded-2xl flex items-center justify-center mx-auto mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-500 shadow-lg shadow-orange-200">
<Truck className="w-10 h-10 text-white" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3 group-hover:text-orange-600 transition-colors duration-300">Fresh Pack</h3>
<p className="text-gray-600 text-sm leading-relaxed mb-4">
Sealed for maximum freshness and aroma preservation
</p>
<div className="inline-block bg-gradient-to-r from-orange-100 to-orange-200 text-orange-700 px-4 py-2 rounded-full text-xs font-bold tracking-wide">
FRESH SEALED
</div>
</div>
</div>
</div>
</div>
{/* Enhanced Product Showcase */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-0 items-center">
{/* Enhanced Product Image */}
<div className="relative order-2 lg:order-1">
<div className="relative p-12">
{/* Floating particles */}
<div className="absolute top-4 left-4 w-2 h-2 bg-emerald-400 rounded-full animate-ping"></div>
<div className="absolute top-12 right-8 w-1 h-1 bg-blue-400 rounded-full animate-ping delay-1000"></div>
<div className="absolute bottom-8 left-8 w-1.5 h-1.5 bg-purple-400 rounded-full animate-ping delay-2000"></div>
<Image
src="/rice_bags.png"
alt="Kashmina Premium Basmati Rice"
width={500}
height={400}
className="w-full h-auto object-contain group-hover:scale-105 transition-transform duration-700 drop-shadow-2xl"
priority
/>
{/* Enhanced Floating Badges */}
<div className="absolute top-8 left-8 bg-white/95 backdrop-blur-md rounded-2xl px-6 py-4 shadow-xl border border-emerald-200/50">
<div className="flex items-center gap-3">
<div className="w-4 h-4 bg-gradient-to-r from-emerald-400 to-emerald-600 rounded-full animate-pulse shadow-lg shadow-emerald-200"></div>
<div>
<span className="font-bold text-gray-900 text-sm block">Premium Quality</span>
<span className="text-emerald-600 text-xs font-medium">Export Grade</span>
</div>
</div>
</div>
{/* Enhanced Rating Badge */}
{/* <div className="absolute top-8 right-8 bg-white/95 backdrop-blur-md rounded-2xl px-6 py-4 shadow-xl border border-yellow-200/50">
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<Star key={i} className="w-3 h-3 fill-yellow-400 text-yellow-400" />
))}
</div>
<div>
<span className="text-sm font-bold text-gray-900 block">4.9/5</span>
<span className="text-xs text-gray-600">2.5k+ Reviews</span>
</div>
</div>
</div> */}
{/* Background decorations */}
<div className="absolute -top-6 -right-6 w-32 h-32 bg-gradient-to-br from-emerald-300 to-blue-400 rounded-full opacity-10 blur-2xl animate-pulse"></div>
<div className="absolute -bottom-6 -left-6 w-28 h-28 bg-gradient-to-br from-purple-300 to-pink-400 rounded-full opacity-10 blur-2xl animate-pulse delay-1000"></div>
</div>
</div>
{/* Enhanced Content */}
<div className="space-y-8 order-1 lg:order-2">
<div>
<h3 className="text-xl sm:text-2xl md:text-3xl font-bold text-slate-900 leading-tight mb-6">
Crafted for {' '}
<span className="text-transparent bg-gradient-to-r from-emerald-600 via-blue-600 to-purple-600 bg-clip-text">
Perfection
</span>
</h3>
<p className="text-base sm:text-lg leading-relaxed text-slate-600 mb-8">
Kashmina represents the pinnacle of basmati rice quality, carefully selected
from the finest grains and processed with precision to deliver an
<span className="text-emerald-600 font-medium">exceptional culinary experience</span>.
</p>
</div>
{/* Enhanced Key Features */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center gap-4 p-4 bg-white/60 backdrop-blur-sm rounded-xl border border-emerald-200/30 hover:bg-white/80 hover:border-emerald-300/50 transition-all duration-300 group">
<div className="w-3 h-3 rounded-full bg-emerald-500 flex-shrink-0"></div>
<span className="text-slate-700 font-medium text-sm sm:text-base group-hover:text-slate-900 transition-colors">Premium long-grain basmati rice</span>
</div>
<div className="flex items-center gap-4 p-4 bg-white/60 backdrop-blur-sm rounded-xl border border-blue-200/30 hover:bg-white/80 hover:border-blue-300/50 transition-all duration-300 group">
<div className="w-3 h-3 rounded-full bg-blue-500 flex-shrink-0"></div>
<span className="text-slate-700 font-medium text-sm sm:text-base group-hover:text-slate-900 transition-colors">Natural aroma and superior taste</span>
</div>
<div className="flex items-center gap-4 p-4 bg-white/60 backdrop-blur-sm rounded-xl border border-purple-200/30 hover:bg-white/80 hover:border-purple-300/50 transition-all duration-300 group">
<div className="w-3 h-3 rounded-full bg-purple-500 flex-shrink-0"></div>
<span className="text-slate-700 font-medium text-sm sm:text-base group-hover:text-slate-900 transition-colors">Carefully aged and processed</span>
</div>
<div className="flex items-center gap-4 p-4 bg-white/60 backdrop-blur-sm rounded-xl border border-orange-200/30 hover:bg-white/80 hover:border-orange-300/50 transition-all duration-300 group">
<div className="w-3 h-3 rounded-full bg-orange-500 flex-shrink-0"></div>
<span className="text-slate-700 font-medium text-sm sm:text-base group-hover:text-slate-900 transition-colors">Export quality standards</span>
</div>
</div>
{/* Enhanced CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 pt-6">
<Button asChild size="lg" className="bg-gradient-to-r from-emerald-600 to-blue-600 hover:from-emerald-700 hover:to-blue-700 text-white px-6 sm:px-8 py-3 sm:py-4 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 group">
<Link href="/kashmina-rice" className="inline-flex items-center justify-center text-sm sm:text-lg">
Explore Kashmina Rice
<ArrowRight className="ml-2 h-4 w-4 sm:h-5 sm:w-5 group-hover:translate-x-1 transition-transform" />
</Link>
</Button>
<Button asChild variant="outline" size="lg" className="border-2 border-gray-300 text-gray-700 hover:bg-gray-50 hover:border-gray-400 px-6 sm:px-8 py-3 sm:py-4 rounded-xl font-semibold text-sm sm:text-lg">
<Link href="/products?brand=kashmina">
Shop Kashmina Products
</Link>
</Button>
</div>
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,236 @@
'use client'
import { useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Factory, Users, TrendingUp, Award, ArrowRight, CheckCircle, Gauge, Globe, Play } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { motion } from 'framer-motion'
export default function ManufacturingSection() {
const [isVideoOpen, setIsVideoOpen] = useState(false)
const facilities = [
{
icon: Factory,
title: "Advanced Rice Milling",
description: "Modern rice milling facility with precision technology for perfect grain separation and quality",
value: "100+ MT/Day",
label: "Rice Processing"
},
{
icon: CheckCircle,
title: "Rice Quality Assurance",
description: "Multi-stage quality checks ensuring every rice grain meets export quality standards",
value: "99.9%",
label: "Pure Rice Quality"
},
{
icon: Globe,
title: "Basmati Export Hub",
description: "Specialized facility for Basmati rice aging, processing and international export",
value: "25+",
label: "Countries Export"
},
{
icon: Users,
title: "Rice Processing Experts",
description: "Skilled team with traditional knowledge and modern techniques in rice processing",
value: "100+",
label: "Rice Specialists"
}
]
const processSteps = [
{
step: "01",
title: "Premium Paddy Sourcing",
description: "Direct procurement from Punjab & Haryana's best rice growing regions",
image: "/farmer.png"
},
{
step: "02",
title: "Traditional Rice Aging",
description: "Natural aging process for Basmati rice to develop characteristic aroma and length",
image: "/factory.png"
},
{
step: "03",
title: "Rice Quality Laboratory",
description: "Comprehensive testing for grain length, aroma, moisture and purity standards",
image: "/lab.png"
},
{
step: "04",
title: "Premium Rice Packaging",
description: "Food-grade packaging with moisture control for long-lasting rice freshness",
image: "/rice_bags.png"
}
]
return (
<section className="relative py-16 lg:py-24 overflow-hidden">
{/* Background */}
<div className="absolute inset-0 bg-gradient-to-br from-amber-50 via-yellow-50 to-orange-50/30"></div>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_20%_30%,rgba(245,158,11,0.05),transparent_50%)]"></div>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_80%_70%,rgba(251,191,36,0.05),transparent_50%)]"></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: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<div className="inline-flex items-center gap-2 bg-amber-100/50 backdrop-blur-sm px-4 py-2 rounded-full mb-6">
<Factory className="w-4 h-4 text-amber-600" />
<span className="text-amber-700 font-medium text-sm">Rice Processing Excellence</span>
</div>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-slate-900 mb-6">
From Paddy Fields to
<span className="text-transparent bg-gradient-to-r from-amber-600 via-yellow-500 to-orange-500 bg-clip-text"> Premium Rice</span>
</h2>
<p className="text-xl text-slate-600 max-w-3xl mx-auto leading-relaxed">
State-of-the-art rice processing facilities ensuring every grain of Basmati and Sella rice meets international quality standards
</p>
{/* Video Button */}
<div className="mt-8">
<Dialog open={isVideoOpen} onOpenChange={setIsVideoOpen}>
<DialogTrigger asChild>
<Button
size="lg"
className="bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-700 text-white px-8 py-4 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 group"
>
<Play className="w-5 h-5 mr-2 group-hover:scale-110 transition-transform" />
Watch Video
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl w-full p-0 bg-black border-0">
<DialogHeader className="sr-only">
<DialogTitle>Rice Processing Video</DialogTitle>
<DialogDescription>
Watch our complete rice processing journey from paddy to premium rice
</DialogDescription>
</DialogHeader>
<div className="relative w-full h-0 pb-[56.25%]"> {/* 16:9 aspect ratio */}
<video
className="absolute inset-0 w-full h-full"
controls
autoPlay
preload="metadata"
poster="/factory.png" // You can add a poster image
>
<source src="https://4m5m4tx28rtva30c.public.blob.vercel-storage.com/media/2025-09-07/rice-processing" type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
</DialogContent>
</Dialog>
</div>
</motion.div>
{/* Facilities Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-20">
{facilities.map((facility, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="bg-white rounded-2xl p-6 shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 group hover:border-amber-200"
>
<div className="flex items-center justify-between mb-4">
<div className="w-12 h-12 bg-gradient-to-br from-amber-100 to-yellow-200 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<facility.icon className="w-6 h-6 text-amber-600" />
</div>
<div className="text-right">
<div className="text-2xl font-bold text-amber-600">{facility.value}</div>
<div className="text-xs text-slate-500 font-medium">{facility.label}</div>
</div>
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">{facility.title}</h3>
<p className="text-slate-600 text-sm leading-relaxed">{facility.description}</p>
</motion.div>
))}
</div>
{/* Process Flow */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
className="bg-white rounded-3xl shadow-2xl p-8 lg:p-12 border border-gray-100"
>
<div className="text-center mb-12">
<h3 className="text-2xl md:text-3xl font-bold text-slate-900 mb-4">
Our Quality Process
</h3>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Every step carefully monitored to deliver premium quality rice that exceeds expectations
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{processSteps.map((step, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.2 }}
className="relative text-center group"
>
{/* Step Number */}
<div className="inline-flex items-center justify-center w-12 h-12 bg-gradient-to-r from-emerald-600 to-emerald-700 text-white rounded-full font-bold text-lg mb-6 group-hover:scale-110 transition-transform duration-300">
{step.step}
</div>
{/* Image */}
<div className="relative w-24 h-24 mx-auto mb-6 rounded-2xl overflow-hidden shadow-lg group-hover:shadow-xl transition-shadow duration-300">
<Image
src={step.image}
alt={step.title}
fill
className="object-cover group-hover:scale-110 transition-transform duration-500"
/>
</div>
{/* Content */}
<h4 className="text-lg font-semibold text-slate-900 mb-3">{step.title}</h4>
<p className="text-slate-600 text-sm leading-relaxed">{step.description}</p>
{/* Connector Line */}
{index < processSteps.length - 1 && (
<div className="hidden lg:block absolute top-6 left-full w-full h-0.5 bg-gradient-to-r from-emerald-200 to-emerald-300 transform translate-x-4"></div>
)}
</motion.div>
))}
</div>
{/* CTA */}
<div className="text-center mt-12">
<Button asChild size="lg" className="bg-gradient-to-r from-emerald-600 to-emerald-700 hover:from-emerald-700 hover:to-emerald-800 text-white px-8 py-4 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 group">
<Link href="/about" className="inline-flex items-center">
Visit Our Facility
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
</Link>
</Button>
</div>
</motion.div>
</div>
</section>
)
}

View File

@@ -0,0 +1,154 @@
'use client'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Calendar, ArrowRight, ExternalLink, Clock, TrendingUp, Wheat, Users, Sparkles } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { motion } from 'framer-motion'
export default function NewsSection() {
// Content data in JSON format for easy management
const riceStoriesData = {
header: {
badge: "Padmaaja Rasooi",
title: "Rice Stories",
description: "For nearly 10,000 years, rice has nourished more communities than any other grain. Deeply woven into human history across continents, it remains our timeless bond with the golden grain."
},
stories: [
{
id: 1,
category: "Culinary Heritage",
title: "Global Favourite",
description: "Rice inspires delicious dishes in every culture.",
image: "https://www.indiagatefoods.com/wp-content/themes/india-gate/static/images/rice-dish-varieties.jpg",
buttonText: "DISCOVER MORE",
bgGradient: "from-amber-900 via-orange-800 to-red-900",
categoryColor: "text-amber-300",
categoryIcon: "Wheat"
},
{
id: 2,
category: "Cultural Journey",
title: "Where Rice Meets Heart",
description: "Every grain connects us to family and tradition.",
image: "https://4m5m4tx28rtva30c.public.blob.vercel-storage.com/media/2025-09-07/indian-family-dinner",
buttonText: "EXPLORE MORE",
bgGradient: "from-emerald-900 via-teal-800 to-blue-900",
categoryColor: "text-emerald-300",
categoryIcon: "Users"
},
{
id: 3,
category: "Traditional Farming",
title: "Heritage Cultivation",
description: "Ancient farming wisdom shapes every harvest.",
image: "https://4m5m4tx28rtva30c.public.blob.vercel-storage.com/media/2025-09-07/deepak-kumar-b4eRRodrirQ-unsplash%20%281%29.webp",
buttonText: "LEARN MORE",
bgGradient: "from-green-900 to-emerald-800",
categoryColor: "text-green-300",
categoryIcon: "Wheat"
},
{
id: 4,
category: "Premium Quality",
title: "Miracle Grain",
description: "Discover unique rice varieties and their benefits.",
image: "https://4m5m4tx28rtva30c.public.blob.vercel-storage.com/media/2025-09-07/Image_fx%20%2840%29%20%281%29.jpg",
buttonText: "DISCOVER MORE",
bgGradient: "from-amber-900 to-orange-800",
categoryColor: "text-amber-300",
categoryIcon: "Sparkles"
}
]
}
const getIcon = (iconName: string) => {
switch (iconName) {
case 'Wheat':
return <Wheat className="w-5 h-5" />
case 'Users':
return <Users className="w-5 h-5" />
case 'Sparkles':
return <Sparkles className="w-5 h-5" />
default:
return <Wheat className="w-5 h-5" />
}
}
return (
<section className="relative py-16 lg:py-24 overflow-hidden bg-gradient-to-br from-amber-50/50 via-orange-50/30 to-red-50/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<h2 className="text-3xl md:text-3xl lg:text-4xl font-bold mb-6">
<span className="bg-gradient-to-r from-amber-700 via-red-600 to-orange-700 bg-clip-text text-transparent">
{riceStoriesData.header.title}
</span>
</h2>
<div className="w-24 h-1 bg-gradient-to-r from-amber-500 to-red-500 mx-auto mb-6"></div>
<p className="text-lg md:text-xl text-slate-700 max-w-4xl mx-auto leading-relaxed">
{riceStoriesData.header.description}
</p>
</motion.div>
{/* Main Content Grid - 4 Cards in 2 Rows */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{riceStoriesData.stories.map((story, index) => (
<motion.div
key={story.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="group cursor-pointer"
>
<div className={`relative overflow-hidden rounded-xl shadow-lg h-96 bg-gradient-to-br ${story.bgGradient}`}>
<Image
src={story.image}
alt={story.title}
fill
className="object-cover group-hover:scale-110 transition-transform duration-700"
loading="lazy"
quality={80}
sizes="(max-width: 768px) 100vw, 50vw"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent"></div>
{/* Content Overlay */}
<div className="absolute bottom-0 left-0 right-0 p-8">
<h3 className="text-2xl md:text-3xl font-bold text-white mb-4 leading-tight drop-shadow-lg">
{story.title}
</h3>
<p className="text-white/90 leading-relaxed mb-6 drop-shadow-lg">
{story.description}
</p>
<Button
variant="secondary"
className="bg-white/20 backdrop-blur-sm border-white/30 text-white hover:bg-white/30 group"
>
{story.buttonText}
<ArrowRight className="ml-2 h-4 w-4 group-hover:translate-x-1 transition-transform" />
</Button>
</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,125 @@
'use client'
import { motion } from 'framer-motion'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import {
Wheat,
Shield,
Award,
Leaf
} from 'lucide-react'
const values = [
{
title: "Rice Purity",
description: "Every grain of Basmati and Sella rice is carefully selected and tested for maximum purity.",
icon: Shield
},
{
title: "Farm Fresh Rice",
description: "Direct sourcing from Punjab & Haryana's finest rice farms ensures authentic quality.",
icon: Wheat
},
{
title: "Rice Excellence",
description: "Committed to delivering the finest aged Basmati and premium Sella rice varieties.",
icon: Award
},
{
title: "Sustainable Rice Farming",
description: "Supporting eco-friendly rice cultivation practices for healthier communities and environment.",
icon: Leaf
}
]
export default function OurValues() {
return (
<section className="py-20 bg-gradient-to-br from-amber-50 to-yellow-50">
<div className="max-w-7xl mx-auto px-3 sm:px-6 lg:px-12">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center space-y-4 mb-16"
>
<Badge variant="secondary" className="bg-amber-100 text-amber-800 px-3 py-1 text-sm font-medium">
Our Rice Values
</Badge>
<h2 className="text-3xl md:text-4xl font-bold text-slate-900">
The Quality of Kashhmna Premium
</h2>
<p className="text-slate-600 max-w-2xl mx-auto text-lg">
Our dedication to rice excellence is built on four core principles that ensure every grain of Basmati and Sella rice meets the highest standards.
</p>
</motion.div>
{/* Values Grid - 4 columns on desktop */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{values.map((value, index) => {
const IconComponent = value.icon
return (
<motion.div
key={value.title}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{
duration: 0.6,
delay: index * 0.1
}}
viewport={{ once: true }}
whileHover={{ y: -8 }}
className="group"
>
<Card className="h-full bg-white/80 backdrop-blur-sm border-0 shadow-lg hover:shadow-xl transition-all duration-300 group-hover:bg-white">
<CardContent className="p-8 text-center">
{/* Icon */}
<div className="w-16 h-16 bg-gradient-to-br from-amber-500 to-orange-600 rounded-full flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
<IconComponent className="h-8 w-8 text-white" />
</div>
{/* Title */}
<h3 className="text-xl font-bold text-slate-900 mb-4 group-hover:text-emerald-700 transition-colors">
{value.title}
</h3>
{/* Description */}
<p className="text-slate-600 leading-relaxed">
{value.description}
</p>
</CardContent>
</Card>
</motion.div>
)
})}
</div>
{/* Bottom CTA */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
viewport={{ once: true }}
className="text-center mt-16"
>
<p className="text-slate-600 text-lg mb-6">
Experience the difference that our values make in every grain
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Badge variant="outline" className="text-emerald-700 border-emerald-300 px-4 py-2">
🌾 Farm to Table
</Badge>
<Badge variant="outline" className="text-blue-700 border-blue-300 px-4 py-2">
🛡 Quality Certified
</Badge>
<Badge variant="outline" className="text-green-700 border-green-300 px-4 py-2">
🌱 Eco-Friendly
</Badge>
</div>
</motion.div>
</div>
</section>
)
}

View File

@@ -0,0 +1,298 @@
'use client'
import { motion } from 'framer-motion'
import Link from 'next/link'
import { ChevronRight, LucideIcon } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
interface BreadcrumbItem {
label: string
href?: string
}
interface HeroAction {
label: string
icon?: LucideIcon
onClick?: () => void
href?: string
variant?: 'primary' | 'secondary'
className?: string
}
interface HeroFeature {
icon: LucideIcon
label: string
color?: 'blue' | 'green' | 'purple' | 'orange' | 'emerald'
}
interface PageHeroProps {
// Basic content
title: string
subtitle?: string
description: string
// Badge configuration
badge?: {
text: string
variant?: 'default' | 'outline'
className?: string
}
// Icon configuration
icon?: {
component: LucideIcon
className?: string
bgColor?: string
}
// Breadcrumb navigation
breadcrumbs?: BreadcrumbItem[]
// Features/highlights (small cards below description)
features?: HeroFeature[]
// Action buttons
actions?: HeroAction[]
// Styling customization
backgroundGradient?: string
titleGradient?: string
className?: string
// Content positioning
alignment?: 'center' | 'left' | 'auto' // auto = smart defaults based on content
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '7xl'
}
const getIconColorClasses = (color: HeroFeature['color'] = 'blue') => {
const colorMap = {
blue: 'text-blue-500',
green: 'text-green-500',
purple: 'text-purple-500',
orange: 'text-orange-500',
emerald: 'text-emerald-500'
}
return colorMap[color]
}
const getMaxWidthClass = (maxWidth: PageHeroProps['maxWidth'] = '5xl') => {
const maxWidthMap = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
'2xl': 'max-w-2xl',
'3xl': 'max-w-3xl',
'4xl': 'max-w-4xl',
'5xl': 'max-w-5xl',
'7xl': 'max-w-7xl'
}
return maxWidthMap[maxWidth]
}
export default function PageHero({
title,
subtitle,
description,
badge,
icon,
breadcrumbs,
features,
actions,
backgroundGradient = 'from-blue-600/10 to-emerald-600/10',
titleGradient = 'from-blue-600 to-emerald-600',
className = '',
alignment = 'auto',
maxWidth = '4xl'
}: PageHeroProps) {
const maxWidthClass = getMaxWidthClass(maxWidth)
// Smart alignment logic - Modern approach (2024-2025)
// Center: Marketing/brand pages, landing pages, promotional content
// Left: Content-heavy pages, documentation, application interfaces
const getAlignment = () => {
if (alignment !== 'auto') return alignment
// Auto-detect best alignment based on content
const hasActions = actions && actions.length > 0
const hasFeatures = features && features.length > 0
const hasIcon = icon !== undefined
const hasBadge = badge !== undefined
// Marketing/promotional content -> center
if (hasActions || hasFeatures || hasIcon || hasBadge) {
return 'center'
}
// Long description -> left (better readability)
if (description.length > 120) {
return 'left'
}
// Default to center for brand/marketing pages
return 'center'
}
const isCenter = getAlignment() === 'center'
return (
<section className="relative py-12 overflow-hidden min-h-[30vh] flex items-center">
{/* Background Gradient - Standardized */}
<div className="absolute inset-0 bg-gradient-to-r from-blue-600/10 to-emerald-600/10" />
{/* Breadcrumb Navigation - Standardized positioning and styling */}
{breadcrumbs && breadcrumbs.length > 0 && (
<nav className="absolute top-20 left-0 right-0 z-30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center h-16">
<div className="flex items-center space-x-2 text-sm">
{breadcrumbs.map((item, index) => (
<div key={index} className="flex items-center space-x-2">
{item.href ? (
<Link
href={item.href}
className="text-slate-300 hover:text-white transition-colors font-medium"
>
{item.label}
</Link>
) : (
<span className="text-white font-semibold">{item.label}</span>
)}
{index < breadcrumbs.length - 1 && (
<ChevronRight className="h-3.5 w-3.5 text-slate-400" />
)}
</div>
))}
</div>
</div>
</div>
</nav>
)}
{/* Main Content - Standardized container and spacing */}
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className={isCenter ? 'text-center' : 'text-left'}
>
{/* Badge with Icon - Standardized styling */}
{badge && (
<div className={`flex ${isCenter ? 'justify-center' : 'justify-start'} mb-6`}>
<Badge
variant="outline"
className="text-blue-700 border-blue-200 bg-blue-50/50 px-4 py-2 text-sm font-medium rounded-full flex items-center space-x-2"
>
{icon && (
<div className={`p-1.5 rounded-lg ${icon.bgColor || 'bg-blue-600'} ${icon.className || ''}`}>
<icon.component className="w-4 h-4 text-white" />
</div>
)}
<span>{badge.text}</span>
</Badge>
</div>
)}
{/* Icon (only when no badge) */}
{icon && !badge && (
<div className={`flex ${isCenter ? 'justify-center' : 'justify-start'} mb-6`}>
<div className={`p-3 rounded-2xl ${icon.bgColor || 'bg-blue-600'} ${icon.className || ''}`}>
<icon.component className="w-8 h-8 text-white" />
</div>
</div>
)}
{/* Title - Standardized typography scale */}
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-slate-800 mb-6">
{subtitle ? (
<>
{subtitle}{' '}
<span className="bg-gradient-to-r from-blue-600 to-emerald-600 bg-clip-text text-transparent">
{title}
</span>
</>
) : title.includes(' ') ? (
<>
{title.split(' ').slice(0, -1).join(' ')}{' '}
<span className="bg-gradient-to-r from-blue-600 to-emerald-600 bg-clip-text text-transparent">
{title.split(' ').slice(-1)}
</span>
</>
) : (
<span className="bg-gradient-to-r from-blue-600 to-emerald-600 bg-clip-text text-transparent">
{title}
</span>
)}
</h1>
{/* Description - Standardized typography */}
<p className={`text-lg text-slate-600 ${maxWidthClass} ${isCenter ? 'mx-auto' : ''} ${
(features && features.length > 0) || (actions && actions.length > 0) ? 'mb-8' : 'mb-0'
} leading-relaxed`}>
{description}
</p>
{/* Features */}
{features && features.length > 0 && (
<div className={`flex flex-wrap ${isCenter ? 'justify-center' : 'justify-start'} gap-4 ${
(actions && actions.length > 0) ? 'mb-8' : 'mb-0'
}`}>
{features.map((feature, index) => (
<div key={index} className="flex items-center space-x-2 bg-white rounded-full px-4 py-2 shadow-sm">
<feature.icon className={`w-5 h-5 ${getIconColorClasses(feature.color)}`} />
<span className="text-sm font-medium text-slate-700">{feature.label}</span>
</div>
))}
</div>
)}
{/* Action Buttons - Standardized styling */}
{actions && actions.length > 0 && (
<div className={`flex flex-col sm:flex-row gap-4 ${isCenter ? 'justify-center' : 'justify-start'}`}>
{actions.map((action, index) => {
const ButtonContent = (
<>
{action.icon && <action.icon className="w-5 h-5 mr-2" />}
{action.label}
</>
)
const buttonClass = action.variant === 'secondary'
? "border-slate-300"
: "bg-blue-600 text-white hover:bg-blue-700"
if (action.href) {
return (
<Link key={index} href={action.href}>
<Button
size="lg"
variant={action.variant === 'secondary' ? 'outline' : 'default'}
className={`${buttonClass} ${action.className || ''}`}
>
{ButtonContent}
</Button>
</Link>
)
}
return (
<Button
key={index}
size="lg"
variant={action.variant === 'secondary' ? 'outline' : 'default'}
className={`${buttonClass} ${action.className || ''}`}
onClick={action.onClick}
>
{ButtonContent}
</Button>
)
})}
</div>
)}
</motion.div>
</div>
</section>
)
}

View File

@@ -0,0 +1,293 @@
'use client'
import { useState, useEffect } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { motion } from 'framer-motion'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
interface Product {
id: string
name: string
slug: string
price: number
description: string | null
images: string[]
category: {
id: string
name: string
}
}
interface Category {
id: string
name: string
displayName?: string
}
interface ProductsSectionProps {
products: Product[]
categories: Category[]
}
export default function ProductsSection({ products, categories }: ProductsSectionProps) {
const [activeCategory, setActiveCategory] = useState<string>('all')
const [currentSlide, setCurrentSlide] = useState(0)
const [productsPerSlide, setProductsPerSlide] = useState(4)
const [touchStart, setTouchStart] = useState<number | null>(null)
const [touchEnd, setTouchEnd] = useState<number | null>(null)
// Update products per slide based on window size
useEffect(() => {
const updateProductsPerSlide = () => {
if (window.innerWidth < 640) {
setProductsPerSlide(1) // mobile
} else if (window.innerWidth < 1024) {
setProductsPerSlide(2) // tablet
} else {
setProductsPerSlide(4) // desktop
}
}
updateProductsPerSlide()
window.addEventListener('resize', updateProductsPerSlide)
return () => window.removeEventListener('resize', updateProductsPerSlide)
}, [])
// Create category display names mapping
const getCategoryDisplayName = (category: Category) => {
const displayNames: { [key: string]: string } = {
'basmati': 'Perfectionist',
'premium': 'Quality Seeker',
'organic': 'Taste Champion',
'specialty': 'Smart Shopper',
'kashmina steam': 'Kashmina Steam',
'kashmina': 'Kashmina Steam',
'steam': 'Kashmina Steam',
'rice': 'Premium Rice'
}
return displayNames[category.name.toLowerCase()] || category.name
}
// Add "All" category with enhanced categories
const allCategories = [
{ id: 'all', name: 'All', displayName: 'All' },
...categories.map(cat => ({
...cat,
displayName: getCategoryDisplayName(cat)
}))
]
// Filter products based on active category
const filteredProducts = activeCategory === 'all'
? products
: products.filter(product => product.category.id === activeCategory)
// Products per slide (responsive)
const getProductsPerSlide = () => {
if (typeof window !== 'undefined') {
if (window.innerWidth < 640) return 1 // mobile
if (window.innerWidth < 1024) return 2 // tablet
return 4 // desktop
}
return 4
}
const totalSlides = Math.ceil(filteredProducts.length / productsPerSlide)
const nextSlide = () => {
setCurrentSlide((prev) => (prev + 1) % totalSlides)
}
const prevSlide = () => {
setCurrentSlide((prev) => (prev - 1 + totalSlides) % totalSlides)
}
const handleCategoryChange = (categoryId: string) => {
setActiveCategory(categoryId)
setCurrentSlide(0) // Reset to first slide when category changes
}
// Touch handlers for mobile swipe
const minSwipeDistance = 50
const onTouchStart = (e: React.TouchEvent) => {
setTouchEnd(null) // otherwise the swipe is fired even with usual touch events
setTouchStart(e.targetTouches[0].clientX)
}
const onTouchMove = (e: React.TouchEvent) => {
setTouchEnd(e.targetTouches[0].clientX)
}
const onTouchEnd = () => {
if (!touchStart || !touchEnd) return
const distance = touchStart - touchEnd
const isLeftSwipe = distance > minSwipeDistance
const isRightSwipe = distance < -minSwipeDistance
if (isLeftSwipe && currentSlide < totalSlides - 1) {
nextSlide()
} else if (isRightSwipe && currentSlide > 0) {
prevSlide()
}
}
const getCurrentProducts = () => {
const startIndex = currentSlide * productsPerSlide
return filteredProducts.slice(startIndex, startIndex + productsPerSlide)
}
return (
<section className="py-16 bg-gradient-to-b from-emerald-50 to-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-emerald-900 mb-4">
BASMATI RICE
</h2>
<div className="flex justify-center mb-8">
<div className="w-32 h-px bg-emerald-600 relative">
<div className="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2">
<div className="w-3 h-3 bg-emerald-600 rounded-full"></div>
</div>
</div>
</div>
</div>
{/* Category Tabs */}
<div className="flex flex-wrap justify-center gap-4 mb-12">
{allCategories.map((category) => (
<button
key={category.id}
onClick={() => handleCategoryChange(category.id)}
className={`px-6 py-3 rounded-full border-2 transition-all duration-300 font-medium ${
activeCategory === category.id
? 'bg-emerald-600 text-white border-emerald-600'
: 'bg-white text-emerald-700 border-emerald-600 hover:bg-emerald-50'
}`}
>
{category.displayName || category.name}
</button>
))}
</div>
{/* Products Carousel */}
<div className="relative">
{/* Navigation Arrows */}
{totalSlides > 1 && (
<>
<button
onClick={prevSlide}
className="absolute left-4 top-1/2 transform -translate-y-1/2 z-10 bg-white/80 hover:bg-white rounded-full p-3 shadow-lg transition-all duration-300"
aria-label="Previous products"
>
<ChevronLeft className="w-6 h-6 text-emerald-700" />
</button>
<button
onClick={nextSlide}
className="absolute right-4 top-1/2 transform -translate-y-1/2 z-10 bg-white/80 hover:bg-white rounded-full p-3 shadow-lg transition-all duration-300"
aria-label="Next products"
>
<ChevronRight className="w-6 h-6 text-emerald-700" />
</button>
</>
)}
{/* Products Grid */}
<div
className="overflow-hidden"
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
>
<motion.div
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
key={`${activeCategory}-${currentSlide}`}
>
{getCurrentProducts().map((product) => (
<motion.div
key={product.id}
className="text-center group"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{/* Product Image */}
<div className="relative aspect-[3/4] overflow-hidden rounded-lg mb-4">
{product.images && product.images.length > 0 ? (
<Image
src={product.images[0]}
alt={product.name}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-emerald-100 to-emerald-200 flex items-center justify-center rounded-lg">
<div className="text-emerald-600 text-4xl font-bold">
{product.name.charAt(0)}
</div>
</div>
)}
</div>
{/* Product Info */}
<div className="space-y-3">
<h3 className="text-xl font-semibold text-gray-800 group-hover:text-emerald-600 transition-colors">
{product.name}
</h3>
<Button
asChild
variant="outline"
className="text-emerald-600 border-emerald-600 hover:bg-emerald-50 hover:text-emerald-700 rounded-full px-6 py-2 font-medium"
>
<Link href={`/products/${product.slug}`}>
KNOW MORE
</Link>
</Button>
</div>
</motion.div>
))}
</motion.div>
</div>
{/* Slide Indicators */}
{totalSlides > 1 && (
<div className="hidden sm:flex justify-center mt-8 space-x-1.5">
{Array.from({ length: totalSlides }).map((_, index) => (
<button
key={index}
onClick={() => setCurrentSlide(index)}
className={`w-1.5 h-1.5 sm:w-2 sm:h-2 md:w-2.5 md:h-2.5 rounded-full transition-all duration-300 ${
currentSlide === index ? 'bg-emerald-600' : 'bg-emerald-200'
}`}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
)}
</div>
{/* View All Products Button */}
<div className="text-center mt-12">
<Button
asChild
size="lg"
className="bg-emerald-600 hover:bg-emerald-700 text-white px-8 py-3 rounded-full text-lg font-medium"
>
<Link href="/products">
View All Products
</Link>
</Button>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,188 @@
'use client'
import { Badge } from '@/components/ui/badge'
import { TrendingUp, Globe, Users, Award, Factory, Wheat, Star, Target } from 'lucide-react'
import { motion } from 'framer-motion'
export default function StatsSection() {
const stats = [
{
icon: Globe,
value: "25+",
label: "Countries Served",
description: "Exporting premium Basmati rice across continents",
color: "from-amber-500 to-yellow-600"
},
{
icon: Wheat,
value: "200+",
label: "MT Rice Daily",
description: "Processing capacity for premium rice varieties",
color: "from-yellow-500 to-orange-600"
},
{
icon: Users,
value: "5000+",
label: "Rice Farmers",
description: "Direct partnerships across Punjab & Haryana rice belt",
color: "from-emerald-500 to-green-600"
},
{
icon: Award,
value: "20+",
label: "Years in Rice Trade",
description: "Two decades of rice expertise and quality",
color: "from-orange-500 to-red-600"
},
{
icon: Factory,
value: "99.9%",
label: "Pure Rice Quality",
description: "Consistent premium rice standards",
color: "from-red-500 to-pink-600"
},
{
icon: Star,
value: "10K+",
label: "Rice Loving Families",
description: "Households enjoying our premium rice daily",
color: "from-yellow-500 to-amber-600"
}
]
const achievements = [
{
title: "Basmati Rice Leadership",
description: "Leading exporter of aged Basmati 1121 rice from Northern India",
icon: Target
},
{
title: "Rice Quality Certifications",
description: "ISO, HACCP, FSSAI certified processing facility",
icon: Award
},
{
title: "Export Excellence",
description: "Consistent export quality meeting international standards",
icon: Globe
},
{
title: "Innovation Pioneer",
description: "Advanced processing technology for superior quality",
icon: TrendingUp
}
]
return (
<section className="relative py-16 lg:py-24 overflow-hidden">
{/* Background */}
<div className="absolute inset-0 bg-gradient-to-br from-gray-900 via-slate-800 to-gray-900"></div>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_20%_30%,rgba(245,158,11,0.1),transparent_50%)]"></div>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_80%_70%,rgba(251,191,36,0.1),transparent_50%)]"></div>
{/* Animated background elements */}
<div className="absolute top-20 left-20 w-32 h-32 bg-amber-500/10 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute bottom-20 right-20 w-40 h-40 bg-yellow-500/10 rounded-full blur-3xl animate-pulse delay-1000"></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: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<div className="inline-flex items-center gap-2 bg-white/10 backdrop-blur-sm px-4 py-2 rounded-full mb-6">
<TrendingUp className="w-4 h-4 text-amber-400" />
<span className="text-amber-300 font-medium text-sm">Rice Excellence</span>
</div>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-6">
From Fields to
<span className="text-transparent bg-gradient-to-r from-amber-400 via-yellow-400 to-orange-400 bg-clip-text"> Global Tables</span>
</h2>
<p className="text-xl text-gray-300 max-w-3xl mx-auto leading-relaxed">
Two decades of rice expertise, bringing premium Basmati and Sella varieties from Northern India's fertile plains to kitchens worldwide
</p>
</motion.div> */}
{/* Stats Grid */}
{/* <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-20">
{stats.map((stat, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="relative group"
>
<div className="bg-white/5 backdrop-blur-sm rounded-2xl p-8 border border-white/10 hover:border-white/20 transition-all duration-500 group-hover:bg-white/10">
<div className={`absolute inset-0 rounded-2xl bg-gradient-to-br ${stat.color} opacity-0 group-hover:opacity-5 transition-opacity duration-500`}></div>
<div className="relative z-10">
<div className={`w-16 h-16 bg-gradient-to-br ${stat.color} rounded-2xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300`}>
<stat.icon className="w-8 h-8 text-white" />
</div>
<div className="text-4xl md:text-5xl font-bold text-white mb-2 group-hover:scale-105 transition-transform duration-300">
{stat.value}
</div>
<div className="text-lg font-semibold text-gray-300 mb-3">
{stat.label}
</div>
<p className="text-gray-400 text-sm leading-relaxed">
{stat.description}
</p>
</div>
</div>
</motion.div>
))}
</div> */}
{/* Achievements Section */}
{/* <motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
className="bg-white/5 backdrop-blur-sm rounded-3xl p-8 lg:p-12 border border-white/10"
>
<div className="text-center mb-12">
<h3 className="text-2xl md:text-3xl font-bold text-white mb-4">
Key Achievements
</h3>
<p className="text-lg text-gray-300 max-w-2xl mx-auto">
Milestones that define our commitment to excellence and industry leadership
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{achievements.map((achievement, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="text-center group"
>
<div className="w-16 h-16 bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-2xl flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
<achievement.icon className="w-8 h-8 text-white" />
</div>
<h4 className="text-lg font-semibold text-white mb-3">
{achievement.title}
</h4>
<p className="text-gray-400 text-sm leading-relaxed">
{achievement.description}
</p>
</motion.div>
))}
</div>
</motion.div> */}
</div>
</section>
)
}

View File

@@ -0,0 +1,245 @@
'use client'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Leaf, Heart, Droplets, Recycle, Users, Globe, ArrowRight, TreePine } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { motion } from 'framer-motion'
export default function SustainabilitySection() {
const initiatives = [
{
icon: Leaf,
title: "Organic Farming Support",
description: "Supporting farmers in transitioning to organic farming practices for sustainable agriculture",
impact: "500+ Farmers",
color: "from-green-500 to-emerald-500"
},
{
icon: Droplets,
title: "Water Conservation",
description: "Implementing water-efficient processing techniques and rainwater harvesting systems",
impact: "30% Water Saved",
color: "from-blue-500 to-cyan-500"
},
{
icon: Recycle,
title: "Zero Waste Processing",
description: "Converting rice husk and by-products into renewable energy and organic fertilizers",
impact: "95% Waste Recycled",
color: "from-purple-500 to-violet-500"
},
{
icon: TreePine,
title: "Carbon Neutral Goals",
description: "Working towards carbon-neutral operations through renewable energy adoption",
impact: "50% Renewable Energy",
color: "from-emerald-500 to-green-500"
}
]
const csrPrograms = [
{
title: "Farmer Welfare Program",
description: "Direct support to rice farmers through fair pricing, agricultural training, and modern farming techniques",
image: "/farmer.png",
beneficiaries: "5000+ Farmers",
focus: "Education & Support"
},
{
title: "Community Development",
description: "Healthcare, education and infrastructure development in rural farming communities",
image: "https://images.unsplash.com/photo-1559027615-cd4628902d4a?w=600&h=400&fit=crop&q=80",
beneficiaries: "50+ Villages",
focus: "Rural Development"
},
{
title: "Food Security Initiative",
description: "Ensuring food security through grain donation programs and nutritional awareness campaigns",
image: "https://images.unsplash.com/photo-1593113598332-cd288d649433?w=600&h=400&fit=crop&q=80",
beneficiaries: "10,000+ Meals",
focus: "Food Security"
}
]
const certifications = [
{ name: "ISO 14001", desc: "Environmental Management" },
{ name: "FSSAI", desc: "Food Safety Standards" },
{ name: "HACCP", desc: "Hazard Analysis" },
{ name: "Organic", desc: "Certified Organic" }
]
return (
<section className="relative py-16 lg:py-24 overflow-hidden">
{/* Background */}
<div className="absolute inset-0 bg-gradient-to-br from-green-50 via-emerald-50 to-white"></div>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_20%_30%,rgba(34,197,94,0.1),transparent_50%)]"></div>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_80%_70%,rgba(16,185,129,0.1),transparent_50%)]"></div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Environmental Initiatives */}
<div className="mb-20">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{initiatives.map((initiative, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="group"
>
<div className="bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-500 p-6 border border-gray-100 group-hover:border-green-200 h-full">
{/* Icon */}
<div className={`w-16 h-16 bg-gradient-to-br ${initiative.color} rounded-2xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300`}>
<initiative.icon className="w-8 h-8 text-white" />
</div>
{/* Content */}
<h3 className="text-lg font-semibold text-slate-900 mb-3">{initiative.title}</h3>
<p className="text-slate-600 text-sm leading-relaxed mb-4">{initiative.description}</p>
{/* Impact */}
<div className="inline-block bg-green-100 text-green-700 px-3 py-1 rounded-full text-xs font-semibold">
{initiative.impact}
</div>
</div>
</motion.div>
))}
</div>
</div>
{/* CSR Programs */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
className="mb-20"
>
<div className="text-center mb-12">
<div className="inline-flex items-center gap-2 bg-blue-100/50 backdrop-blur-sm px-4 py-2 rounded-full mb-6">
<Heart className="w-4 h-4 text-blue-600" />
<span className="text-blue-700 font-medium text-sm">Social Impact</span>
</div>
<h3 className="text-2xl md:text-3xl font-bold text-slate-900 mb-4">
Community Development Programs
</h3>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Creating positive impact in farming communities through targeted social initiatives
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{csrPrograms.map((program, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.2 }}
className="group"
>
<div className="bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-500 overflow-hidden border border-gray-100 group-hover:border-blue-200">
{/* Image */}
<div className="relative h-48 overflow-hidden">
<Image
src={program.image}
alt={program.title}
fill
className="object-cover group-hover:scale-110 transition-transform duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent"></div>
{/* Impact Badge */}
<div className="absolute top-4 right-4 bg-white/90 backdrop-blur-sm text-slate-700 px-3 py-1 rounded-full text-xs font-semibold">
{program.beneficiaries}
</div>
</div>
{/* Content */}
<div className="p-6">
<div className="flex items-center gap-2 mb-3">
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full font-medium">
{program.focus}
</span>
</div>
<h4 className="text-lg font-semibold text-slate-900 mb-3">{program.title}</h4>
<p className="text-slate-600 text-sm leading-relaxed">{program.description}</p>
</div>
</div>
</motion.div>
))}
</div>
</motion.div>
{/* Certifications & Commitments */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
className="bg-white rounded-3xl shadow-2xl p-8 lg:p-12 border border-gray-100"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
{/* Left Content */}
<div>
<div className="inline-flex items-center gap-2 bg-emerald-100/50 backdrop-blur-sm px-4 py-2 rounded-full mb-6">
<Globe className="w-4 h-4 text-emerald-600" />
<span className="text-emerald-700 font-medium text-sm">Global Standards</span>
</div>
<h3 className="text-2xl md:text-3xl font-bold text-slate-900 mb-6">
Certified for Excellence
</h3>
<p className="text-lg text-slate-600 leading-relaxed mb-8">
Our commitment to quality and sustainability is validated by international certifications and industry recognition.
</p>
{/* Certifications Grid */}
<div className="grid grid-cols-2 gap-4 mb-8">
{certifications.map((cert, index) => (
<div key={index} className="text-center p-4 bg-gray-50 rounded-xl">
<div className="text-lg font-bold text-emerald-600 mb-1">{cert.name}</div>
<div className="text-xs text-slate-600">{cert.desc}</div>
</div>
))}
</div>
<Button asChild size="lg" className="bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white px-8 py-4 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 group">
<Link href="/about/certifications" className="inline-flex items-center">
View All Certifications
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
</Link>
</Button>
</div>
{/* Right Visual */}
<div className="relative">
<div className="relative w-full h-80 rounded-2xl overflow-hidden shadow-2xl">
<Image
src="https://images.unsplash.com/photo-1560493676-04071c5f467b?w=600&h=400&fit=crop&q=80"
alt="Sustainable farming"
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-green-900/40 to-transparent"></div>
</div>
{/* Floating Stats */}
<div className="absolute -bottom-6 -left-6 bg-white rounded-2xl shadow-xl p-6 border border-gray-100">
<div className="text-2xl font-bold text-green-600">Carbon</div>
<div className="text-sm text-slate-600">Neutral by 2030</div>
</div>
<div className="absolute -top-6 -right-6 bg-white rounded-2xl shadow-xl p-6 border border-gray-100">
<div className="text-2xl font-bold text-blue-600">Zero</div>
<div className="text-sm text-slate-600">Waste Goal</div>
</div>
</div>
</div>
</motion.div>
</div>
</section>
)
}

View File

@@ -0,0 +1,62 @@
'use client'
import Link from 'next/link'
import { motion } from 'framer-motion'
import { Mail, Phone } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
export default function CTA() {
return (
<section className="py-12 sm:py-16 md:py-20 bg-gradient-to-r from-slate-900 to-emerald-900 text-white relative overflow-hidden">
<div className="absolute inset-0 bg-black/20"></div>
<div className="relative z-10 max-w-6xl mx-auto px-3 sm:px-6 md:px-8 lg:px-12">
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center"
>
<Badge className="mb-4 sm:mb-6 bg-emerald-600 text-white px-3 sm:px-4 py-1.5 sm:py-2 text-xs sm:text-sm font-medium">Get In Touch</Badge>
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold mb-6 sm:mb-8 leading-tight">
Ready to Partner With Us?
</h2>
<p className="text-base sm:text-lg text-emerald-100 mb-8 sm:mb-12 max-w-3xl mx-auto leading-relaxed px-2">
Connect with us for bulk orders, wholesale pricing, and custom solutions.
Let&apos;s build a healthier future together.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6 mb-8 sm:mb-12 max-w-2xl mx-auto">
<div className="flex items-center justify-center p-4 sm:p-6 bg-white/10 rounded-lg sm:rounded-xl backdrop-blur-sm hover:bg-white/15 transition-all duration-300">
<Phone className="h-5 w-5 sm:h-6 sm:w-6 mr-3 sm:mr-4 text-emerald-300 flex-shrink-0" />
<div className="text-left">
<div className="text-xs sm:text-sm text-emerald-200 font-medium">Call Us</div>
<div className="text-base sm:text-lg font-bold">+91 94757 58817</div>
</div>
</div>
<div className="flex items-center justify-center p-4 sm:p-6 bg-white/10 rounded-lg sm:rounded-xl backdrop-blur-sm hover:bg-white/15 transition-all duration-300">
<Mail className="h-5 w-5 sm:h-6 sm:w-6 mr-3 sm:mr-4 text-emerald-300 flex-shrink-0" />
<div className="text-left">
<div className="text-xs sm:text-sm text-emerald-200 font-medium">Email Us</div>
<div className="text-base sm:text-lg font-bold break-all sm:break-normal">info@padmajarice.com</div>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center items-center max-w-md sm:max-w-none mx-auto">
<Button size="lg" className="w-full sm:w-auto bg-white text-slate-900 hover:bg-emerald-50 px-6 sm:px-8 py-2.5 sm:py-3 text-base sm:text-lg font-bold rounded-lg sm:rounded-xl shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1">
<Link href="/contact">Get in Touch</Link>
</Button>
<Button size="lg" variant="outline" className="w-full sm:w-auto border-white bg-transparent hover:bg-white hover:text-slate-900 px-6 sm:px-8 py-2.5 sm:py-3 text-base sm:text-lg font-bold rounded-lg sm:rounded-xl shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1">
<Link href="/part-time">Part time Job</Link>
</Button>
</div>
</motion.div>
</div>
</section>
)
}

View File

@@ -0,0 +1,502 @@
'use client'
import { useState } from 'react'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Checkbox } from '@/components/ui/checkbox'
import { toast } from 'sonner'
import { Loader2, Package, Building, Phone, Mail, MessageCircle } from 'lucide-react'
const inquirySchema = z.object({
// Company Information
companyName: z.string().min(2, 'Company name must be at least 2 characters'),
contactPerson: z.string().min(2, 'Contact person name must be at least 2 characters'),
designation: z.string().min(2, 'Designation is required'),
email: z.string().email('Please enter a valid email address'),
phone: z.string().min(10, 'Please enter a valid phone number'),
// Business Details
businessType: z.string().min(1, 'Please select business type'),
gstNumber: z.string().optional(),
address: z.string().min(10, 'Please provide complete address'),
// Product Requirements
quantityRequired: z.string().min(1, 'Quantity is required'),
quantityUnit: z.string().default('tons'),
deliveryLocation: z.string().min(2, 'Delivery location is required'),
expectedDeliveryDate: z.string().optional(),
// Additional Information
message: z.string().min(10, 'Please provide detailed requirements (minimum 10 characters)'),
hearAboutUs: z.string().optional(),
// Terms
agreedToTerms: z.boolean().refine(val => val === true, {
message: 'You must agree to the terms and conditions'
})
})
type InquiryFormData = z.infer<typeof inquirySchema>
interface B2BInquiryFormProps {
isOpen: boolean
onOpenChange: (open: boolean) => void
product?: {
id: string
name: string
category: { name: string }
price: number
weight?: string
}
}
export default function B2BInquiryForm({ isOpen, onOpenChange, product }: B2BInquiryFormProps) {
const [isSubmitting, setIsSubmitting] = useState(false)
const form = useForm<InquiryFormData>({
resolver: zodResolver(inquirySchema),
defaultValues: {
companyName: '',
contactPerson: '',
designation: '',
email: '',
phone: '',
businessType: '',
gstNumber: '',
address: '',
quantityRequired: '',
quantityUnit: 'tons',
deliveryLocation: '',
expectedDeliveryDate: '',
message: product ? `I am interested in bulk procurement of ${product.name}. Please provide detailed quotation including pricing, minimum order quantity, and delivery terms.` : '',
hearAboutUs: '',
agreedToTerms: false
}
})
const onSubmit = async (data: InquiryFormData) => {
setIsSubmitting(true)
try {
const formData = {
...data,
productId: product?.id,
productName: product?.name,
productCategory: product?.category?.name,
productPrice: product?.price,
submissionType: 'b2b_inquiry',
submittedAt: new Date().toISOString()
}
const response = await fetch('/api/inquiries', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
})
if (!response.ok) {
throw new Error('Failed to submit inquiry')
}
const result = await response.json()
toast.success('Inquiry submitted successfully!', {
description: 'Our team will contact you within 24 hours with a detailed quotation.'
})
form.reset()
onOpenChange(false)
} catch (error) {
console.error('Error submitting inquiry:', error)
toast.error('Failed to submit inquiry', {
description: 'Please try again or contact us directly.'
})
} finally {
setIsSubmitting(false)
}
}
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-xl">
<Package className="h-6 w-6 text-emerald-600" />
B2B Inquiry Form
</DialogTitle>
<DialogDescription>
{product ? (
<>Requesting quote for: <span className="font-semibold text-emerald-600">{product.name}</span></>
) : (
'Fill out this form to get a detailed quotation for bulk orders'
)}
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Company Information Section */}
<div className="space-y-4">
<h3 className="text-lg font-semibold flex items-center gap-2 text-slate-700 border-b pb-2">
<Building className="h-5 w-5" />
Company Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="companyName"
render={({ field }) => (
<FormItem>
<FormLabel>Company Name *</FormLabel>
<FormControl>
<Input placeholder="Enter your company name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="businessType"
render={({ field }) => (
<FormItem>
<FormLabel>Business Type *</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select business type" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="retailer">Retailer</SelectItem>
<SelectItem value="wholesaler">Wholesaler</SelectItem>
<SelectItem value="distributor">Distributor</SelectItem>
<SelectItem value="restaurant">Restaurant/Hotel</SelectItem>
<SelectItem value="caterer">Catering Service</SelectItem>
<SelectItem value="food_processor">Food Processor</SelectItem>
<SelectItem value="export">Export Business</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="contactPerson"
render={({ field }) => (
<FormItem>
<FormLabel>Contact Person *</FormLabel>
<FormControl>
<Input placeholder="Full name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="designation"
render={({ field }) => (
<FormItem>
<FormLabel>Designation *</FormLabel>
<FormControl>
<Input placeholder="e.g., Procurement Manager" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-1">
<Mail className="h-4 w-4" />
Email Address *
</FormLabel>
<FormControl>
<Input type="email" placeholder="business@company.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-1">
<Phone className="h-4 w-4" />
Phone Number *
</FormLabel>
<FormControl>
<Input type="tel" placeholder="+91 98765 43210" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="gstNumber"
render={({ field }) => (
<FormItem>
<FormLabel>GST Number (Optional)</FormLabel>
<FormControl>
<Input placeholder="Enter GST number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem>
<FormLabel>Complete Business Address *</FormLabel>
<FormControl>
<Textarea
placeholder="Enter complete address including city, state, and pincode"
className="min-h-[80px]"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Product Requirements Section */}
<div className="space-y-4">
<h3 className="text-lg font-semibold flex items-center gap-2 text-slate-700 border-b pb-2">
<Package className="h-5 w-5" />
Product Requirements
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField
control={form.control}
name="quantityRequired"
render={({ field }) => (
<FormItem>
<FormLabel>Quantity Required *</FormLabel>
<FormControl>
<Input type="number" placeholder="e.g., 10" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="quantityUnit"
render={({ field }) => (
<FormItem>
<FormLabel>Unit</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="tons">Tons</SelectItem>
<SelectItem value="kg">Kilograms</SelectItem>
<SelectItem value="quintal">Quintal</SelectItem>
<SelectItem value="bags">Bags</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expectedDeliveryDate"
render={({ field }) => (
<FormItem>
<FormLabel>Expected Delivery Date</FormLabel>
<FormControl>
<Input type="date" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="deliveryLocation"
render={({ field }) => (
<FormItem>
<FormLabel>Delivery Location *</FormLabel>
<FormControl>
<Input placeholder="City, State where delivery is required" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Additional Information Section */}
<div className="space-y-4">
<h3 className="text-lg font-semibold flex items-center gap-2 text-slate-700 border-b pb-2">
<MessageCircle className="h-5 w-5" />
Additional Information
</h3>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>Detailed Requirements *</FormLabel>
<FormControl>
<Textarea
placeholder="Please provide specific requirements including quality specifications, packaging preferences, payment terms, or any other details that would help us provide an accurate quotation."
className="min-h-[120px]"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="hearAboutUs"
render={({ field }) => (
<FormItem>
<FormLabel>How did you hear about us? (Optional)</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select an option" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="google_search">Google Search</SelectItem>
<SelectItem value="social_media">Social Media</SelectItem>
<SelectItem value="referral">Referral</SelectItem>
<SelectItem value="trade_show">Trade Show</SelectItem>
<SelectItem value="advertisement">Advertisement</SelectItem>
<SelectItem value="existing_customer">Existing Customer</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Terms and Conditions */}
<div className="space-y-4">
<FormField
control={form.control}
name="agreedToTerms"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="text-sm">
I agree to the terms and conditions and authorize Padmaaja Rasooi to contact me regarding this inquiry. *
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
</div>
{/* Submit Button */}
<div className="flex gap-4 pt-4">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
className="flex-1"
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="flex-1 bg-emerald-600 hover:bg-emerald-700"
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Submitting...
</>
) : (
'Submit Inquiry'
)}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,195 @@
'use client'
import { useState, useEffect } from 'react'
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { ShoppingCart, X, Plus, Minus } from 'lucide-react'
import { cartManager } from '@/lib/cart'
import Link from 'next/link'
import Image from 'next/image'
import { toast } from 'sonner'
interface CartItem {
id: string
name: string
price: number
quantity: number
image: string | null
}
interface CartSidebarProps {
children?: React.ReactNode
}
export default function CartSidebar({ children }: CartSidebarProps) {
const [cart, setCart] = useState<CartItem[]>([])
const [isOpen, setIsOpen] = useState(false)
const loadCart = () => {
const cartItems = cartManager.getCart()
setCart(cartItems)
}
useEffect(() => {
loadCart()
const handleCartUpdate = () => loadCart()
window.addEventListener('cartUpdated', handleCartUpdate)
return () => {
window.removeEventListener('cartUpdated', handleCartUpdate)
}
}, [])
const updateQuantity = (productId: string, newQuantity: number) => {
cartManager.updateQuantity(productId, newQuantity)
loadCart()
}
const removeItem = (productId: string) => {
cartManager.removeFromCart(productId)
toast.success('Item removed from cart')
loadCart()
}
const itemCount = cart.reduce((sum, item) => sum + item.quantity, 0)
const cartTotal = cartManager.getTotalPrice()
return (
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetTrigger asChild>
{children || (
<Button variant="ghost" size="sm" className="relative">
<ShoppingCart className="h-5 w-5" />
{itemCount > 0 && (
<Badge
className="absolute -top-[5px] -right-[5px] h-5 w-5 flex items-center justify-center p-0 bg-red-500 text-white !text-xs"
>
{itemCount > 99 ? '99+' : itemCount}
</Badge>
)}
</Button>
)}
</SheetTrigger>
<SheetContent className="w-full sm:max-w-lg">
<SheetHeader>
<SheetTitle className="flex items-center">
<ShoppingCart className="h-5 w-5 mr-2" />
Shopping Cart ({itemCount})
</SheetTitle>
<SheetDescription>
Manage your cart items
</SheetDescription>
</SheetHeader>
<div className="mt-6 flex-1 overflow-y-auto">
{cart.length === 0 ? (
<div className="text-center py-12">
<ShoppingCart className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">Your cart is empty</p>
<Button
className="mt-4"
onClick={() => setIsOpen(false)}
asChild
>
<Link href="/products">Continue Shopping</Link>
</Button>
</div>
) : (
<div className="space-y-4">
{cart.map((item) => (
<div key={item.id} className="flex items-start space-x-3 p-3 border rounded-lg">
<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 flex-shrink-0"
/>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm line-clamp-2">{item.name}</h4>
<div className="flex items-center space-x-1 mt-1">
<span className="text-sm font-bold text-gray-900">
{(item.price || 0).toFixed(2)}
</span>
</div>
<div className="flex items-center justify-between mt-2">
<div className="flex items-center space-x-1">
<Button
variant="outline"
size="sm"
className="h-6 w-6 p-0"
onClick={() => updateQuantity(item.id, item.quantity - 1)}
disabled={item.quantity <= 1}
>
<Minus className="h-3 w-3" />
</Button>
<span className="text-sm font-medium w-8 text-center">
{item.quantity || 0}
</span>
<Button
variant="outline"
size="sm"
className="h-6 w-6 p-0"
onClick={() => updateQuantity(item.id, item.quantity + 1)}
>
<Plus className="h-3 w-3" />
</Button>
</div>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
onClick={() => removeItem(item.id)}
>
<X className="h-3 w-3" />
</Button>
</div>
</div>
</div>
))}
<Separator />
<div className="space-y-2 p-3 bg-gray-50 rounded-lg">
<div className="flex justify-between font-bold">
<span>Total</span>
<span>{cartTotal.toFixed(2)}</span>
</div>
</div>
<div className="space-y-2">
<Button
className="w-full"
onClick={() => setIsOpen(false)}
asChild
>
<Link href="/checkout">Checkout</Link>
</Button>
<Button
variant="outline"
className="w-full"
onClick={() => setIsOpen(false)}
asChild
>
<Link href="/cart">View Cart</Link>
</Button>
</div>
</div>
)}
</div>
</SheetContent>
</Sheet>
)
}

View File

@@ -0,0 +1,463 @@
'use client'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { ShoppingCart, Star, Eye, X } from 'lucide-react'
import Link from 'next/link'
import OptimizedImage from '@/components/ui/OptimizedImage'
import { motion } from 'framer-motion'
import { cartManager } from '@/lib/cart'
import { toast } from 'sonner'
import { useRouter } from 'next/navigation'
import { Product } from '@/types'
import { useState, useEffect } from 'react'
import { isFeatureEnabled } from '@/lib/business-config'
import B2BInquiryForm from '@/components/shop/B2BInquiryForm'
interface ProductReviewStats {
averageRating: number
totalReviews: number
}
interface ProductCardProps {
product: Product
index: number
}
export default function ProductCard({ product, index }: ProductCardProps) {
const router = useRouter()
const [isQuickViewOpen, setIsQuickViewOpen] = useState(false)
const [isInquiryFormOpen, setIsInquiryFormOpen] = useState(false)
const [reviewStats, setReviewStats] = useState<ProductReviewStats>({
averageRating: 0,
totalReviews: 0
})
// Fetch review statistics for the product
useEffect(() => {
const fetchReviewStats = async () => {
try {
const response = await fetch(`/api/reviews?productId=${product.id}&limit=1000`)
const data = await response.json()
if (response.ok && data.reviews) {
const reviews = data.reviews
const totalReviews = reviews.length
if (totalReviews > 0) {
const sum = reviews.reduce((acc: number, review: any) => acc + review.rating, 0)
const averageRating = sum / totalReviews
setReviewStats({
averageRating: Math.round(averageRating * 10) / 10, // Round to 1 decimal
totalReviews
})
}
}
} catch (error) {
console.error('Failed to fetch review stats:', error)
}
}
fetchReviewStats()
}, [product.id])
const getDiscountedPrice = (price: number, discount: number) => {
return price - (price * discount / 100)
}
// Helper function to calculate per kg price
const getPerKgPrice = (price: number, weight: string | null, discount: number = 0) => {
if (!weight) return null
// Extract numeric value from weight string (e.g., "1kg", "500g", "2.5 kg")
const weightMatch = weight.toLowerCase().match(/(\d+(?:\.\d+)?)\s*(kg|g|gram|kilos?)/i)
if (!weightMatch) return null
const value = parseFloat(weightMatch[1])
const unit = weightMatch[2].toLowerCase()
// Convert to kg
let weightInKg = value
if (unit.startsWith('g')) {
weightInKg = value / 1000
}
const finalPrice = discount > 0 ? getDiscountedPrice(price, discount) : price
return Math.round(finalPrice / weightInKg)
}
const handleAddToCart = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
console.log('Add to cart clicked for:', product.name) // Debug log
if (product.stock === 0) {
toast.error('Product is out of stock')
return
}
try {
const success = cartManager.addToCart(product, 1)
if (success) {
console.log('Item added successfully') // Debug log
toast.success(`${product.name} added to cart!`)
} else {
toast.error('Not enough stock available')
}
} catch (error) {
console.error('Error adding to cart:', error)
toast.error('Failed to add to cart')
}
}
const handleCardClick = () => {
router.push(`/products/${product.slug}`)
}
const handleViewDetails = (e: React.MouseEvent) => {
e.stopPropagation()
setIsQuickViewOpen(true)
}
const handleRequestQuote = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setIsInquiryFormOpen(true)
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card
className="h-full hover:shadow-lg transition-all duration-300 group overflow-hidden cursor-pointer hover:scale-[1.02]"
onClick={handleCardClick}
>
<div className="relative overflow-hidden">
<OptimizedImage
src={product.images[0] || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={product.name}
width={400}
height={300}
className="w-full h-full object-contain group-hover:scale-105 transition-transform duration-300"
priority={index < 4} // Prioritize first 4 products
quality={50}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 25vw"
/>
{/* Per kg price badge - Top Left */}
{getPerKgPrice(product.price, product.weight, product.discount) && (
<Badge className="absolute top-2 left-2 sm:top-3 sm:left-3 bg-emerald-500 hover:bg-emerald-600 text-white font-semibold shadow-lg text-base">
{getPerKgPrice(product.price, product.weight, product.discount)}/kg
</Badge>
)}
{/* Discount badge - Top Left, below per kg badge if both exist */}
{product.discount > 0 && (
<Badge className={`absolute ${getPerKgPrice(product.price, product.weight, product.discount) ? 'top-8 sm:top-10' : 'top-2 sm:top-3'} left-2 sm:left-3 bg-red-500 hover:bg-red-600 text-white font-semibold shadow-lg text-xs`}>
{product.discount}% OFF
</Badge>
)}
{/* Dynamic Rating Display */}
{reviewStats.totalReviews > 0 ? (
<div className="absolute top-2 right-2 sm:top-3 sm:right-3 flex items-center bg-white/90 backdrop-blur-sm rounded-full px-2 py-1 shadow-lg">
<Star className="h-3 w-3 text-yellow-400 fill-current" />
<span className="text-xs ml-1 font-medium">{reviewStats.averageRating}</span>
<span className="text-xs ml-1 text-gray-500">({reviewStats.totalReviews})</span>
</div>
) : (
<>
</>
)}
{product.stock === 0 && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<Badge variant="destructive" className="text-xs sm:text-sm">Out of Stock</Badge>
</div>
)}
<Badge className="absolute bottom-2 right-2 sm:bottom-3 sm:right-3 bg-white/90 text-slate-700 border border-slate-200 text-xs font-medium shadow-lg backdrop-blur-sm">
{product.category.name}
</Badge>
</div>
<CardHeader className="pb-2 p-3 sm:pb-2">
<div className="flex items-start justify-between gap-2 mb-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<CardTitle className="text-base sm:text-lg font-semibold truncate group-hover:text-blue-600 transition-colors flex-1 leading-tight cursor-help">
{product.name}
</CardTitle>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p>{product.name}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<CardDescription className="line-clamp-2 md:text-xs text-sm text-gray-600 leading-relaxed">
{product.description || 'Premium quality product from Padmaaja Rasooi Pvt. Ltd.'}
</CardDescription>
</CardHeader>
<CardContent className="pt-0 p-3 sm:p-6 sm:pt-0 space-y-3 sm:space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
{product.discount > 0 ? (
<>
<span className="text-base sm:text-lg font-bold text-green-600">
{getDiscountedPrice(product.price, product.discount).toFixed(2)}
</span>
<span className="text-xs sm:text-sm text-gray-500 line-through">
{product.price.toFixed(2)}
</span>
</>
) : (
<span className="text-base sm:text-lg font-bold text-gray-900">
{product.price.toFixed(2)}
</span>
)}
</div>
{/* <span className="text-xs sm:text-sm text-gray-500">
Stock: <span className={product.stock > 0 ? 'text-green-600' : 'text-red-600'}>{product.stock}</span>
</span> */}
</div>
<div className="flex gap-2">
{/* B2C Feature - Add to Cart Button (Disabled for B2B mode) */}
{isFeatureEnabled('cart') ? (
<Button
size="sm"
onClick={handleAddToCart}
disabled={product.stock === 0}
className="flex-1 text-xs sm:text-sm h-8 sm:h-9"
>
<ShoppingCart className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2" />
<span className="hidden sm:inline">{product.stock === 0 ? 'Out of Stock' : 'Add to Cart'}</span>
<span className="sm:hidden">{product.stock === 0 ? 'Out' : 'Add'}</span>
</Button>
) : (
<Button
size="sm"
onClick={handleRequestQuote}
className="flex-1 text-xs sm:text-sm h-8 sm:h-9 bg-emerald-600 hover:bg-emerald-700"
>
<Eye className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2" />
<span className="hidden sm:inline">Get Quote</span>
<span className="sm:hidden">Quote</span>
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={handleViewDetails}
className="px-2 sm:px-3 h-8 sm:h-9"
>
<Eye className="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</div>
</CardContent>
</Card>
{/* Quick View Dialog */}
<Dialog open={isQuickViewOpen} onOpenChange={setIsQuickViewOpen}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-xl font-bold">{product.name}</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Product Image */}
<div className="space-y-4">
<div className="relative aspect-square overflow-hidden rounded-lg border">
<OptimizedImage
src={product.images[0] || 'https://images.pexels.com/photos/3683107/pexels-photo-3683107.jpeg'}
alt={product.name}
width={400}
height={400}
className="w-full h-full object-contain"
quality={80}
/>
{product.discount > 0 && (
<Badge className="absolute top-4 left-4 bg-red-500 hover:bg-red-600 text-white font-semibold">
{product.discount}% OFF
</Badge>
)}
</div>
{/* Additional Images if available */}
{product.images.length > 1 && (
<div className="grid grid-cols-4 gap-2">
{product.images.slice(1, 5).map((image, idx) => (
<div key={idx} className="aspect-square rounded border overflow-hidden">
<OptimizedImage
src={image}
alt={`${product.name} ${idx + 2}`}
width={100}
height={100}
className="w-full h-full object-contain"
quality={60}
/>
</div>
))}
</div>
)}
</div>
{/* Product Details */}
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-2">Product Details</h3>
<p className="text-gray-600 leading-relaxed">
{product.description || 'Premium quality product from Padmaaja Rasooi Pvt. Ltd.'}
</p>
</div>
{/* Category and Brand */}
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-sm font-medium text-gray-500">Category</span>
<p className="font-semibold">{product.category?.name}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500">Brand</span>
<p className="font-semibold">{product.brand || 'Padmaaja Rasooi'}</p>
</div>
{product.weight && (
<div>
<span className="text-sm font-medium text-gray-500">Weight</span>
<p className="font-semibold">{product.weight}</p>
</div>
)}
{product.origin && (
<div>
<span className="text-sm font-medium text-gray-500">Origin</span>
<p className="font-semibold">{product.origin}</p>
</div>
)}
</div>
{/* Pricing */}
<div className="border-t pt-4">
<div className="flex items-center space-x-3 mb-4">
{product.discount > 0 ? (
<>
<span className="text-2xl font-bold text-green-600">
{getDiscountedPrice(product.price, product.discount).toFixed(2)}
</span>
<span className="text-lg text-gray-500 line-through">
{product.price.toFixed(2)}
</span>
</>
) : (
<span className="text-2xl font-bold text-gray-900">
{product.price.toFixed(2)}
</span>
)}
</div>
{/* Per kg price in quick view */}
{getPerKgPrice(product.price, product.weight, product.discount) && (
<div className="mb-4 text-sm text-gray-600">
<span className="font-medium">Per kg: </span>
{getPerKgPrice(product.price, product.weight, product.discount)}/kg
</div>
)}
{/* Stock Status */}
<div className="mb-4">
<span className="text-sm font-medium text-gray-500">Availability: </span>
<span className={`font-semibold ${product.stock > 0 ? 'text-green-600' : 'text-red-600'}`}>
{product.stock > 0 ? `In Stock (${product.stock} available)` : 'Out of Stock'}
</span>
</div>
{/* Action Buttons */}
<div className="space-y-3">
{/* B2C Feature - Add to Cart Button (Disabled for B2B mode) */}
{isFeatureEnabled('cart') ? (
<Button
onClick={handleAddToCart}
disabled={product.stock === 0}
className="w-full"
size="lg"
>
<ShoppingCart className="h-4 w-4 mr-2" />
{product.stock === 0 ? 'Out of Stock' : 'Add to Cart'}
</Button>
) : (
<Button
onClick={() => {
setIsQuickViewOpen(false)
setIsInquiryFormOpen(true)
}}
className="w-full bg-emerald-600 hover:bg-emerald-700"
size="lg"
>
<Eye className="h-4 w-4 mr-2" />
Request Quote
</Button>
)}
<Button
variant="outline"
onClick={() => {
setIsQuickViewOpen(false)
router.push(`/products/${product.slug}`)
}}
className="w-full"
size="lg"
>
View Full Details
</Button>
</div>
</div>
{/* Dynamic Rating in Quick View */}
<div className="flex items-center space-x-2 pt-4 border-t">
<div className="flex items-center">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`h-4 w-4 ${
reviewStats.totalReviews > 0 && i < Math.round(reviewStats.averageRating)
? 'text-yellow-400 fill-current'
: 'text-gray-300'
}`}
/>
))}
</div>
<span className="text-sm text-gray-600">
{reviewStats.totalReviews > 0
? `${reviewStats.averageRating} (${reviewStats.totalReviews} reviews)`
: 'No reviews yet'
}
</span>
</div>
</div>
</div>
</DialogContent>
</Dialog>
{/* B2B Inquiry Form */}
<B2BInquiryForm
isOpen={isInquiryFormOpen}
onOpenChange={setIsInquiryFormOpen}
product={{
id: product.id,
name: product.name,
category: product.category,
price: product.price,
weight: product.weight || undefined
}}
/>
</motion.div>
)
}

View File

@@ -0,0 +1,102 @@
'use client'
import { useState } from 'react'
import ProductCard from '@/components/shop/ProductCard'
import { Product } from '@/types'
import { Button } from '@/components/ui/button'
interface ProductGridProps {
products: Product[]
categoryFilter?: string
initialLoadCount?: number
loadMoreCount?: number
}
export default function ProductGrid({
products,
categoryFilter,
initialLoadCount = 8,
loadMoreCount = 4
}: ProductGridProps) {
const [displayCount, setDisplayCount] = useState(initialLoadCount)
const filteredProducts = categoryFilter
? products.filter(product =>
product.category.name.toLowerCase().includes(categoryFilter.toLowerCase()) ||
product.name.toLowerCase().includes(categoryFilter.toLowerCase())
)
: products
const displayedProducts = filteredProducts.slice(0, displayCount)
const hasMoreProducts = displayCount < filteredProducts.length
const remainingProducts = filteredProducts.length - displayCount
const loadMore = () => {
setDisplayCount(prev => Math.min(prev + loadMoreCount, filteredProducts.length))
}
const showLess = () => {
setDisplayCount(initialLoadCount)
}
if (filteredProducts.length === 0) {
return (
<div className="text-center py-8 sm:py-12 px-4">
<p className="text-gray-500 text-base sm:text-lg">No products found in this category.</p>
<p className="text-gray-400 text-sm mt-2">Check back soon for new products!</p>
</div>
)
}
return (
<div className="space-y-8">
{/* Products Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 md:gap-4 gap-8">
{displayedProducts.map((product, index) => (
<ProductCard
key={product.id}
product={product}
index={index}
/>
))}
</div>
{/* Load More / Show Less Controls */}
{filteredProducts.length > initialLoadCount && (
<div className="flex flex-col items-center gap-4 pt-4">
{/* Products Counter */}
<div className="text-center">
<p className="text-sm text-gray-600">
Showing <span className="font-semibold">{displayedProducts.length}</span> of{' '}
<span className="font-semibold">{filteredProducts.length}</span> products
</p>
</div>
{/* Action Buttons */}
<div className="flex gap-3">
{hasMoreProducts && (
<Button
onClick={loadMore}
variant="outline"
className="px-6 py-2 rounded-lg border-gray-300 hover:border-gray-400 transition-colors"
>
Load {Math.min(loadMoreCount, remainingProducts)} More
{remainingProducts > loadMoreCount && ` (${remainingProducts} remaining)`}
</Button>
)}
{displayCount > initialLoadCount && (
<Button
onClick={showLess}
variant="ghost"
className="px-6 py-2 text-gray-600 hover:text-gray-800 transition-colors"
>
Show Less
</Button>
)}
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,127 @@
'use client'
import { Suspense, lazy, ComponentType } from 'react'
import { Skeleton } from '@/components/ui/skeleton'
interface LazyComponentProps {
fallback?: React.ReactNode
className?: string
}
// Generic lazy loading wrapper
export function createLazyComponent<T extends ComponentType<any>>(
importFunction: () => Promise<{ default: T }>,
fallback?: React.ReactNode
) {
const LazyComponent = lazy(importFunction)
return function LazyWrapper(props: React.ComponentProps<T> & LazyComponentProps) {
const { fallback: customFallback, className, ...componentProps } = props
const defaultFallback = (
<div className={className}>
<Skeleton className="w-full h-64 rounded-lg" />
</div>
)
return (
<Suspense fallback={customFallback || defaultFallback}>
<LazyComponent {...(componentProps as React.ComponentProps<T>)} />
</Suspense>
)
}
}
// Product Grid Skeleton
export function ProductGridSkeleton({ count = 8 }: { count?: number }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="space-y-3">
<Skeleton className="w-full h-48 rounded-lg" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-1/2" />
<div className="flex items-center space-x-2">
<Skeleton className="h-6 w-16" />
<Skeleton className="h-6 w-20" />
</div>
</div>
))}
</div>
)
}
// Chart Skeleton
export function ChartSkeleton({ className }: { className?: string }) {
return (
<div className={className}>
<div className="space-y-4">
<Skeleton className="h-6 w-32" />
<Skeleton className="h-64 w-full rounded-lg" />
<div className="flex space-x-4">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-16" />
</div>
</div>
</div>
)
}
// Dashboard Stats Skeleton
export function DashboardStatsSkeleton() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="p-6 bg-white rounded-lg border space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-8 w-16" />
<Skeleton className="h-3 w-20" />
</div>
))}
</div>
)
}
// Table Skeleton
export function TableSkeleton({ rows = 5, cols = 4 }: { rows?: number, cols?: number }) {
return (
<div className="space-y-4">
{/* Header */}
<div className="flex space-x-4">
{Array.from({ length: cols }).map((_, i) => (
<Skeleton key={i} className="h-4 flex-1" />
))}
</div>
{/* Rows */}
{Array.from({ length: rows }).map((_, rowIndex) => (
<div key={rowIndex} className="flex space-x-4">
{Array.from({ length: cols }).map((_, colIndex) => (
<Skeleton key={colIndex} className="h-8 flex-1" />
))}
</div>
))}
</div>
)
}
// Form Skeleton
export function FormSkeleton() {
return (
<div className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-10 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-24 w-full" />
</div>
<div className="flex space-x-4">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-10 w-20" />
</div>
</div>
)
}

View File

@@ -0,0 +1,103 @@
'use client'
import Image from 'next/image'
import { useState } from 'react'
import { cn } from '@/lib/utils'
interface OptimizedImageProps {
src: string
alt: string
width: number
height: number
className?: string
priority?: boolean
fill?: boolean
sizes?: string
quality?: number
placeholder?: 'blur' | 'empty'
blurDataURL?: string
}
// Generate a simple blur placeholder
const shimmer = (w: number, h: number) => `
<svg width="${w}" height="${h}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="g">
<stop stop-color="#f3f4f6" offset="20%" />
<stop stop-color="#e5e7eb" offset="50%" />
<stop stop-color="#f3f4f6" offset="70%" />
</linearGradient>
</defs>
<rect width="${w}" height="${h}" fill="#f3f4f6" />
<rect id="r" width="${w}" height="${h}" fill="url(#g)" />
<animate xlink:href="#r" attributeName="x" from="-${w}" to="${w}" dur="1s" repeatCount="indefinite" />
</svg>`
const toBase64 = (str: string) =>
typeof window === 'undefined'
? Buffer.from(str).toString('base64')
: window.btoa(str)
export default function OptimizedImage({
src,
alt,
width,
height,
className,
priority = false,
fill = false,
sizes,
quality = 75,
placeholder = 'blur',
blurDataURL,
...props
}: OptimizedImageProps) {
const [imageError, setImageError] = useState(false)
const [imageLoaded, setImageLoaded] = useState(false)
// Default fallback image for products
const fallbackSrc = '/logo.png'
// Generate blur placeholder if not provided
const defaultBlurDataURL = `data:image/svg+xml;base64,${toBase64(shimmer(width, height))}`
const handleError = () => {
setImageError(true)
}
const handleLoad = () => {
setImageLoaded(true)
}
return (
<div className={cn("relative overflow-hidden", className)}>
<Image
src={imageError ? fallbackSrc : src}
alt={alt}
width={fill ? undefined : width}
height={fill ? undefined : height}
fill={fill}
priority={priority}
quality={quality}
sizes={sizes || fill ? '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw' : undefined}
placeholder={placeholder}
blurDataURL={blurDataURL || defaultBlurDataURL}
className={cn(
"transition-all duration-300",
imageLoaded ? "opacity-100" : "opacity-0",
fill ? "object-cover" : "w-full h-full object-cover"
)}
onError={handleError}
onLoad={handleLoad}
{...props}
/>
{/* Loading shimmer effect */}
{!imageLoaded && (
<div className="absolute inset-0 bg-gradient-to-r from-gray-200 via-gray-100 to-gray-200 animate-pulse">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/60 to-transparent animate-shimmer"></div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,58 @@
'use client';
import * as React from 'react';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn('border-b', className)}
{...props}
/>
));
AccordionItem.displayName = 'AccordionItem';
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn('pb-4 pt-0', className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@@ -0,0 +1,141 @@
'use client';
import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className
)}
{...props}
/>
);
AlertDialogHeader.displayName = 'AlertDialogHeader';
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
);
AlertDialogFooter.displayName = 'AlertDialogFooter';
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold', className)}
{...props}
/>
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: 'outline' }),
'mt-2 sm:mt-0',
className
)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

59
components/ui/alert.tsx Normal file
View File

@@ -0,0 +1,59 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const alertVariants = cva(
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: 'default',
},
}
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = 'Alert';
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
{...props}
/>
));
AlertTitle.displayName = 'AlertTitle';
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-sm [&_p]:leading-relaxed', className)}
{...props}
/>
));
AlertDescription.displayName = 'AlertDescription';
export { Alert, AlertTitle, AlertDescription };

View File

@@ -0,0 +1,7 @@
'use client';
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
const AspectRatio = AspectRatioPrimitive.Root;
export { AspectRatio };

50
components/ui/avatar.tsx Normal file
View File

@@ -0,0 +1,50 @@
'use client';
import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { cn } from '@/lib/utils';
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
className
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-muted',
className
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };

36
components/ui/badge.tsx Normal file
View File

@@ -0,0 +1,36 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };

56
components/ui/button.tsx Normal file
View File

@@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,66 @@
'use client';
import * as React from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { DayPicker } from 'react-day-picker';
import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn('p-3', className)}
classNames={{
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
month: 'space-y-4',
caption: 'flex justify-center pt-1 relative items-center',
caption_label: 'text-sm font-medium',
nav: 'space-x-1 flex items-center',
nav_button: cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'
),
nav_button_previous: 'absolute left-1',
nav_button_next: 'absolute right-1',
table: 'w-full border-collapse space-y-1',
head_row: 'flex',
head_cell:
'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
row: 'flex w-full mt-2',
cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
day: cn(
buttonVariants({ variant: 'ghost' }),
'h-9 w-9 p-0 font-normal aria-selected:opacity-100'
),
day_range_end: 'day-range-end',
day_selected:
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
day_today: 'bg-accent text-accent-foreground',
day_outside:
'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
day_disabled: 'text-muted-foreground opacity-50',
day_range_middle:
'aria-selected:bg-accent aria-selected:text-accent-foreground',
day_hidden: 'invisible',
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
);
}
Calendar.displayName = 'Calendar';
export { Calendar };

86
components/ui/card.tsx Normal file
View File

@@ -0,0 +1,86 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-lg border bg-card text-card-foreground shadow-sm',
className
)}
{...props}
/>
));
Card.displayName = 'Card';
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
/>
));
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
'text-2xl font-semibold leading-none tracking-tight',
className
)}
{...props}
/>
));
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
));
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
));
CardFooter.displayName = 'CardFooter';
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

262
components/ui/carousel.tsx Normal file
View File

@@ -0,0 +1,262 @@
'use client';
import * as React from 'react';
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from 'embla-carousel-react';
import { ArrowLeft, ArrowRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: 'horizontal' | 'vertical';
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error('useCarousel must be used within a <Carousel />');
}
return context;
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = 'horizontal',
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === 'horizontal' ? 'x' : 'y',
},
plugins
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return;
}
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'ArrowLeft') {
event.preventDefault();
scrollPrev();
} else if (event.key === 'ArrowRight') {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext]
);
React.useEffect(() => {
if (!api || !setApi) {
return;
}
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) {
return;
}
onSelect(api);
api.on('reInit', onSelect);
api.on('select', onSelect);
return () => {
api?.off('select', onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn('relative', className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}
);
Carousel.displayName = 'Carousel';
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel();
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
'flex',
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
className
)}
{...props}
/>
</div>
);
});
CarouselContent.displayName = 'CarouselContent';
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel();
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
'min-w-0 shrink-0 grow-0 basis-full',
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
className
)}
{...props}
/>
);
});
CarouselItem.displayName = 'CarouselItem';
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
'absolute h-8 w-8 rounded-full',
orientation === 'horizontal'
? '-left-12 top-1/2 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
);
});
CarouselPrevious.displayName = 'CarouselPrevious';
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
'absolute h-8 w-8 rounded-full',
orientation === 'horizontal'
? '-right-12 top-1/2 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
);
});
CarouselNext.displayName = 'CarouselNext';
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
};

View File

@@ -0,0 +1,30 @@
'use client';
import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Check } from 'lucide-react';
import { cn } from '@/lib/utils';
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn('flex items-center justify-center text-current')}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View File

@@ -0,0 +1,11 @@
'use client';
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

155
components/ui/command.tsx Normal file
View File

@@ -0,0 +1,155 @@
'use client';
import * as React from 'react';
import { type DialogProps } from '@radix-ui/react-dialog';
import { Command as CommandPrimitive } from 'cmdk';
import { Search } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Dialog, DialogContent } from '@/components/ui/dialog';
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
className
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
className
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn('-mx-1 h-px bg-border', className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
'ml-auto text-xs tracking-widest text-muted-foreground',
className
)}
{...props}
/>
);
};
CommandShortcut.displayName = 'CommandShortcut';
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View File

@@ -0,0 +1,356 @@
'use client'
import React, { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import { Alert, AlertDescription } from '@/components/ui/alert'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Progress } from '@/components/ui/progress'
import { Download, FileText, CheckCircle, AlertTriangle } from 'lucide-react'
import Papa from 'papaparse'
interface ExportColumn {
key: string
label: string
transform?: (value: any, row?: any) => string
required?: boolean
}
interface CsvExportProps {
title: string
description: string
columns: ExportColumn[]
onExport: (
selectedColumns: string[],
filters?: any,
onProgress?: (progress: number) => void
) => Promise<{
success: boolean
data?: any[]
message?: string
}>
filters?: {
label: string
key: string
options: { value: string; label: string }[]
}[]
maxRecords?: number
filename?: string
children?: React.ReactNode
}
export default function CsvExport({
title,
description,
columns,
onExport,
filters = [],
maxRecords = 10000,
filename,
children
}: CsvExportProps) {
const [isOpen, setIsOpen] = useState(false)
const [selectedColumns, setSelectedColumns] = useState<string[]>(
columns.filter(col => col.required).map(col => col.key)
)
const [filterValues, setFilterValues] = useState<Record<string, string>>({})
const [exporting, setExporting] = useState(false)
const [progress, setProgress] = useState(0)
const [result, setResult] = useState<{
success: boolean
message?: string
recordCount?: number
} | null>(null)
const handleColumnToggle = (columnKey: string, checked: boolean) => {
const column = columns.find(col => col.key === columnKey)
if (column?.required) return // Don't allow unchecking required columns
if (checked) {
setSelectedColumns(prev => [...prev, columnKey])
} else {
setSelectedColumns(prev => prev.filter(key => key !== columnKey))
}
}
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedColumns(columns.map(col => col.key))
} else {
setSelectedColumns(columns.filter(col => col.required).map(col => col.key))
}
}
const handleFilterChange = (filterKey: string, value: string) => {
setFilterValues(prev => ({
...prev,
[filterKey]: value
}))
}
const generateFilename = () => {
if (filename) return filename
const timestamp = new Date().toISOString().split('T')[0]
return `${title.toLowerCase().replace(/\s+/g, '_')}_export_${timestamp}.csv`
}
const downloadCsv = (data: any[]) => {
const selectedColumnData = columns.filter(col => selectedColumns.includes(col.key))
// Prepare headers
const headers = selectedColumnData.map(col => col.label)
// Prepare data rows
const rows = data.map(row => {
return selectedColumnData.map(col => {
const value = row[col.key]
if (col.transform) {
return col.transform(value, row)
}
return value?.toString() || ''
})
})
// Generate CSV
const csvContent = Papa.unparse([headers, ...rows])
// Download
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', generateFilename())
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
const handleExport = async () => {
if (selectedColumns.length === 0) {
setResult({
success: false,
message: 'Please select at least one column to export'
})
return
}
setExporting(true)
setProgress(0)
setResult(null)
try {
const exportResult = await onExport(selectedColumns, filterValues, setProgress)
if (exportResult.success && exportResult.data) {
downloadCsv(exportResult.data)
setResult({
success: true,
message: 'Export completed successfully',
recordCount: exportResult.data.length
})
} else {
setResult({
success: false,
message: exportResult.message || 'Export failed'
})
}
} catch (error) {
setResult({
success: false,
message: error instanceof Error ? error.message : 'Export failed'
})
} finally {
setExporting(false)
setProgress(0)
}
}
const resetDialog = () => {
setResult(null)
setProgress(0)
setFilterValues({})
}
const selectedCount = selectedColumns.length
const allSelected = selectedCount === columns.length
const someSelected = selectedCount > 0 && selectedCount < columns.length
return (
<Dialog open={isOpen} onOpenChange={(open) => {
setIsOpen(open)
if (!open) resetDialog()
}}>
<DialogTrigger asChild>
{children || (
<Button variant="outline">
<Download className="h-4 w-4 mr-2" />
Export CSV
</Button>
)}
</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* Filters */}
{filters.length > 0 && (
<div className="space-y-4">
<h4 className="font-medium">Filters</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{filters.map(filter => (
<div key={filter.key} className="space-y-2">
<Label>{filter.label}</Label>
<Select
value={filterValues[filter.key] || ''}
onValueChange={(value) => handleFilterChange(filter.key, value)}
>
<SelectTrigger>
<SelectValue placeholder={`Select ${filter.label.toLowerCase()}`} />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All</SelectItem>
{filter.options.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
))}
</div>
</div>
)}
{/* Column Selection */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="font-medium">Select Columns to Export</h4>
<div className="flex items-center space-x-2">
<Checkbox
id="select-all"
checked={allSelected}
ref={(ref) => {
if (ref && ref instanceof HTMLButtonElement) {
(ref as any).indeterminate = someSelected
}
}}
onCheckedChange={handleSelectAll}
/>
<Label htmlFor="select-all" className="text-sm">
Select All ({selectedCount}/{columns.length})
</Label>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 max-h-60 overflow-y-auto border rounded-lg p-4">
{columns.map(column => (
<div key={column.key} className="flex items-center space-x-2">
<Checkbox
id={column.key}
checked={selectedColumns.includes(column.key)}
onCheckedChange={(checked) => handleColumnToggle(column.key, !!checked)}
disabled={column.required}
/>
<Label
htmlFor={column.key}
className={`text-sm ${column.required ? 'font-medium' : ''}`}
>
{column.label}
{column.required && (
<span className="text-red-500 ml-1">*</span>
)}
</Label>
</div>
))}
</div>
</div>
{/* Export Info */}
<div className="p-4 bg-blue-50 rounded-lg">
<div className="flex items-start space-x-3">
<FileText className="h-5 w-5 text-blue-500 mt-0.5" />
<div className="text-sm">
<p className="font-medium text-blue-900">Export Information</p>
<ul className="text-blue-700 space-y-1 mt-1">
<li> {selectedCount} columns selected</li>
<li> Maximum {maxRecords.toLocaleString()} records</li>
<li> File format: CSV (UTF-8)</li>
<li> Filename: {generateFilename()}</li>
</ul>
</div>
</div>
</div>
{/* Export Progress */}
{exporting && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Exporting...</span>
<span className="text-sm text-gray-500">{Math.round(progress)}%</span>
</div>
<Progress value={progress} className="w-full" />
</div>
)}
{/* Export Result */}
{result && (
<Alert variant={result.success ? 'default' : 'destructive'}>
{result.success ? (
<CheckCircle className="h-4 w-4" />
) : (
<AlertTriangle className="h-4 w-4" />
)}
<AlertDescription>
<div className="space-y-1">
<p className="font-medium">{result.message}</p>
{result.recordCount !== undefined && (
<p className="text-sm">
Exported {result.recordCount.toLocaleString()} records
</p>
)}
</div>
</AlertDescription>
</Alert>
)}
{/* Action Buttons */}
<div className="flex items-center justify-end space-x-2 pt-4 border-t">
<Button
variant="outline"
onClick={() => setIsOpen(false)}
disabled={exporting}
>
Cancel
</Button>
<Button
onClick={handleExport}
disabled={selectedColumns.length === 0 || exporting}
>
{exporting ? 'Exporting...' : 'Export Data'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,429 @@
'use client'
import React, { useState, useRef } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Alert, AlertDescription } from '@/components/ui/alert'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Progress } from '@/components/ui/progress'
import { Upload, Download, FileText, AlertTriangle, CheckCircle, X } from 'lucide-react'
import Papa from 'papaparse'
interface ColumnDefinition {
key: string
label: string
required?: boolean
}
interface CsvImportProps {
title: string
description: string
templateColumns: Array<ColumnDefinition | string>
onImport: (data: any[], onProgress?: (progress: number) => void) => Promise<{
success: boolean
message: string
successCount?: number
errorCount?: number
errors?: string[]
}>
sampleData?: Record<string, any>[]
validationRules?: {
required?: string[]
formats?: Record<string, (value: any) => boolean>
transforms?: Record<string, (value: any) => any>
}
maxFileSize?: number // in MB
children?: React.ReactNode
}
export default function CsvImport({
title,
description,
templateColumns,
onImport,
sampleData = [],
validationRules = {},
maxFileSize = 10,
children
}: CsvImportProps) {
const [isOpen, setIsOpen] = useState(false)
const [file, setFile] = useState<File | null>(null)
const [importing, setImporting] = useState(false)
const [progress, setProgress] = useState(0)
const [result, setResult] = useState<{
success: boolean
message: string
successCount?: number
errorCount?: number
errors?: string[]
} | null>(null)
const [preview, setPreview] = useState<any[]>([])
const [validationErrors, setValidationErrors] = useState<string[]>([])
const fileInputRef = useRef<HTMLInputElement>(null)
// Normalize templateColumns to always be ColumnDefinition objects
const normalizedColumns: ColumnDefinition[] = templateColumns.map(col =>
typeof col === 'string'
? { key: col, label: col, required: false }
: col
)
const downloadTemplate = () => {
const headers = normalizedColumns.map(col => col.label)
const csvContent = Papa.unparse([
headers,
...sampleData.map(row => normalizedColumns.map(col => row[col.key] || ''))
])
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', `${title.toLowerCase().replace(/\s+/g, '_')}_template.csv`)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
const validateData = (data: any[]): string[] => {
const errors: string[] = []
const { required = [], formats = {}, transforms = {} } = validationRules
// Get required fields from template columns and validation rules
const requiredFields = [
...required,
...normalizedColumns.filter(col => col.required).map(col => col.key)
]
data.forEach((row, index) => {
// Check required fields
requiredFields.forEach(field => {
if (!row[field] || row[field].toString().trim() === '') {
const column = normalizedColumns.find(col => col.key === field)
const fieldLabel = column ? column.label : field
errors.push(`Row ${index + 1}: ${fieldLabel} is required`)
}
})
// Check format validations
Object.entries(formats).forEach(([field, validator]) => {
if (row[field] && !validator(row[field])) {
const column = normalizedColumns.find(col => col.key === field)
const fieldLabel = column ? column.label : field
errors.push(`Row ${index + 1}: ${fieldLabel} has invalid format`)
}
})
// Apply transformations
Object.entries(transforms).forEach(([field, transformer]) => {
if (row[field]) {
try {
row[field] = transformer(row[field])
} catch (error) {
const column = normalizedColumns.find(col => col.key === field)
const fieldLabel = column ? column.label : field
errors.push(`Row ${index + 1}: ${fieldLabel} transformation failed`)
}
}
})
})
return errors
}
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = event.target.files?.[0]
if (!selectedFile) return
// Check file size
if (selectedFile.size > maxFileSize * 1024 * 1024) {
setValidationErrors([`File size must be less than ${maxFileSize}MB`])
return
}
// Check file type
if (!selectedFile.name.toLowerCase().endsWith('.csv')) {
setValidationErrors(['Please select a CSV file'])
return
}
setFile(selectedFile)
setValidationErrors([])
setResult(null)
// Parse and preview the file
Papa.parse(selectedFile, {
header: true,
skipEmptyLines: true,
complete: (results) => {
if (results.errors.length > 0) {
setValidationErrors(results.errors.map(err => err.message))
return
}
const data = results.data as any[]
const errors = validateData(data)
if (errors.length > 0) {
setValidationErrors(errors.slice(0, 10)) // Show first 10 errors
return
}
setPreview(data.slice(0, 5)) // Show first 5 rows
setValidationErrors([])
},
error: (error) => {
setValidationErrors([error.message])
}
})
}
const handleImport = async () => {
if (!file) return
setImporting(true)
setProgress(0)
setResult(null)
try {
Papa.parse(file, {
header: true,
skipEmptyLines: true,
complete: async (results) => {
try {
const data = results.data as any[]
const errors = validateData(data)
if (errors.length > 0) {
setResult({
success: false,
message: 'Validation failed',
errors: errors
})
return
}
const result = await onImport(data, setProgress)
setResult(result)
if (result.success) {
setFile(null)
setPreview([])
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}
} catch (error) {
setResult({
success: false,
message: error instanceof Error ? error.message : 'Import failed'
})
} finally {
setImporting(false)
setProgress(0)
}
}
})
} catch (error) {
setResult({
success: false,
message: error instanceof Error ? error.message : 'Import failed'
})
setImporting(false)
setProgress(0)
}
}
const resetDialog = () => {
setFile(null)
setPreview([])
setValidationErrors([])
setResult(null)
setProgress(0)
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}
return (
<Dialog open={isOpen} onOpenChange={(open) => {
setIsOpen(open)
if (!open) resetDialog()
}}>
<DialogTrigger asChild>
{children || (
<Button variant="outline">
<Upload className="h-4 w-4 mr-2" />
Import CSV
</Button>
)}
</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* Download Template */}
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center space-x-3">
<FileText className="h-5 w-5 text-blue-500" />
<div>
<h4 className="font-medium">Download Template</h4>
<p className="text-sm text-gray-500">
Download CSV template with required columns
</p>
</div>
</div>
<Button variant="outline" onClick={downloadTemplate}>
<Download className="h-4 w-4 mr-2" />
Template
</Button>
</div>
{/* File Upload */}
<div className="space-y-2">
<Label htmlFor="csv-file">Select CSV File</Label>
<Input
ref={fileInputRef}
id="csv-file"
type="file"
accept=".csv"
onChange={handleFileChange}
disabled={importing}
/>
<p className="text-xs text-gray-500">
Maximum file size: {maxFileSize}MB. Only CSV files are supported.
</p>
</div>
{/* Validation Errors */}
{validationErrors.length > 0 && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<div className="space-y-1">
<p className="font-medium">Validation Errors:</p>
<ul className="list-disc list-inside text-sm space-y-1">
{validationErrors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</div>
</AlertDescription>
</Alert>
)}
{/* Preview */}
{preview.length > 0 && (
<div className="space-y-2">
<h4 className="font-medium">Preview (First 5 rows)</h4>
<div className="border rounded-lg overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-gray-50">
<tr>
{normalizedColumns.map(col => (
<th key={col.key} className="px-3 py-2 text-left font-medium">
{col.label}
{col.required && <span className="text-red-500 ml-1">*</span>}
</th>
))}
</tr>
</thead>
<tbody>
{preview.map((row, index) => (
<tr key={index} className="border-t">
{normalizedColumns.map(col => (
<td key={col.key} className="px-3 py-2">
{row[col.key]?.toString() || ''}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Import Progress */}
{importing && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Importing...</span>
<span className="text-sm text-gray-500">{Math.round(progress)}%</span>
</div>
<Progress value={progress} className="w-full" />
</div>
)}
{/* Import Result */}
{result && (
<Alert variant={result.success ? 'default' : 'destructive'}>
{result.success ? (
<CheckCircle className="h-4 w-4" />
) : (
<X className="h-4 w-4" />
)}
<AlertDescription>
<div className="space-y-2">
<p className="font-medium">{result.message}</p>
{result.successCount !== undefined && (
<p className="text-sm">
Successfully imported: {result.successCount} records
</p>
)}
{result.errorCount !== undefined && result.errorCount > 0 && (
<p className="text-sm">
Failed to import: {result.errorCount} records
</p>
)}
{result.errors && result.errors.length > 0 && (
<div className="text-sm">
<p className="font-medium">Errors:</p>
<ul className="list-disc list-inside space-y-1">
{result.errors.slice(0, 10).map((error, index) => (
<li key={index}>{error}</li>
))}
{result.errors.length > 10 && (
<li>... and {result.errors.length - 10} more errors</li>
)}
</ul>
</div>
)}
</div>
</AlertDescription>
</Alert>
)}
{/* Action Buttons */}
<div className="flex items-center justify-end space-x-2 pt-4 border-t">
<Button
variant="outline"
onClick={() => setIsOpen(false)}
disabled={importing}
>
Cancel
</Button>
<Button
onClick={handleImport}
disabled={!file || validationErrors.length > 0 || importing}
>
{importing ? 'Importing...' : 'Import Data'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}

122
components/ui/dialog.tsx Normal file
View File

@@ -0,0 +1,122 @@
'use client';
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { cn } from '@/lib/utils';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-1.5 text-center sm:text-left',
className
)}
{...props}
/>
);
DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
);
DialogFooter.displayName = 'DialogFooter';
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
'text-lg font-semibold leading-none tracking-tight',
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View File

@@ -0,0 +1,200 @@
'use client';
import * as React from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { Check, ChevronRight, Circle } from 'lucide-react';
import { cn } from '@/lib/utils';
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View File

@@ -0,0 +1,275 @@
'use client'
import { useState, useRef, useCallback } from 'react'
import { Button } from '@/components/ui/button'
import { Progress } from '@/components/ui/progress'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { useFileUpload, type UploadOptions, type UploadResult } from '@/hooks/use-file-upload'
import {
Upload,
X,
FileImage,
File as FileIcon,
Trash2,
Eye
} from 'lucide-react'
import { cn } from '@/lib/utils'
import Image from 'next/image'
interface FileUploadProps {
onUploadComplete?: (results: UploadResult[]) => void
onFilesChange?: (files: File[]) => void
onDelete?: (url: string) => void
options?: UploadOptions
multiple?: boolean
accept?: string
disabled?: boolean
className?: string
showPreview?: boolean
existingFiles?: string[]
}
export function FileUpload({
onUploadComplete,
onFilesChange,
onDelete,
options = {},
multiple = true,
accept = 'image/*',
disabled = false,
className,
showPreview = true,
existingFiles = []
}: FileUploadProps) {
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
const [uploadedFiles, setUploadedFiles] = useState<string[]>(existingFiles)
const [dragOver, setDragOver] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const { uploadFiles, deleteFile, uploading, progress } = useFileUpload()
const handleFileSelect = useCallback((files: FileList | null) => {
if (!files) return
const fileArray = Array.from(files)
setSelectedFiles(prev => multiple ? [...prev, ...fileArray] : fileArray)
onFilesChange?.(fileArray)
}, [multiple, onFilesChange])
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault()
setDragOver(false)
if (disabled) return
const files = e.dataTransfer.files
handleFileSelect(files)
}, [disabled, handleFileSelect])
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault()
if (!disabled) setDragOver(true)
}, [disabled])
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault()
setDragOver(false)
}, [])
const removeSelectedFile = useCallback((index: number) => {
setSelectedFiles(prev => prev.filter((_, i) => i !== index))
}, [])
const handleUpload = useCallback(async () => {
if (selectedFiles.length === 0) return
try {
const results = await uploadFiles(selectedFiles, options)
const urls = results.map(r => r.url)
setUploadedFiles(prev => [...prev, ...urls])
setSelectedFiles([])
onUploadComplete?.(results)
} catch (error) {
console.error('Upload failed:', error)
}
}, [selectedFiles, uploadFiles, options, onUploadComplete])
const handleDeleteUploadedFile = useCallback(async (url: string) => {
try {
await deleteFile(url)
setUploadedFiles(prev => prev.filter(f => f !== url))
onDelete?.(url)
} catch (error) {
console.error('Delete failed:', error)
}
}, [deleteFile, onDelete])
const getFileIcon = (file: File) => {
if (file.type.startsWith('image/')) {
return <FileImage className="h-8 w-8 text-blue-500" />
}
return <FileIcon className="h-8 w-8 text-gray-500" />
}
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
return (
<div className={cn('space-y-4', className)}>
{/* Upload Area */}
<Card
className={cn(
'border-2 border-dashed transition-colors cursor-pointer',
dragOver ? 'border-blue-500 bg-blue-50' : 'border-gray-300',
disabled && 'opacity-50 cursor-not-allowed'
)}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={() => !disabled && fileInputRef.current?.click()}
>
<CardContent className="flex flex-col items-center justify-center py-8 px-4 text-center">
<Upload className="h-12 w-12 text-gray-400 mb-4" />
<div className="space-y-2">
<p className="text-lg font-medium">
{dragOver ? 'Drop files here' : 'Click to upload or drag & drop'}
</p>
<p className="text-sm text-gray-500">
{accept === 'image/*' ? 'Images only' : 'Various file types supported'}
{options.maxSize && ` • Max ${options.maxSize}MB per file`}
{options.maxFiles && ` • Max ${options.maxFiles} files`}
</p>
</div>
</CardContent>
</Card>
<input
ref={fileInputRef}
type="file"
accept={accept}
multiple={multiple}
onChange={(e) => handleFileSelect(e.target.files)}
className="hidden"
disabled={disabled}
/>
{/* Selected Files */}
{selectedFiles.length > 0 && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="font-medium">Selected Files ({selectedFiles.length})</h4>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setSelectedFiles([])}
disabled={uploading}
>
Clear All
</Button>
<Button
onClick={handleUpload}
disabled={uploading || selectedFiles.length === 0}
size="sm"
>
{uploading ? 'Uploading...' : 'Upload Files'}
</Button>
</div>
</div>
{uploading && (
<div className="space-y-2">
<Progress value={progress} className="w-full" />
<p className="text-sm text-gray-500 text-center">{progress}% uploaded</p>
</div>
)}
<div className="grid gap-2">
{selectedFiles.map((file, index) => (
<div
key={`${file.name}-${index}`}
className="flex items-center gap-3 p-3 border rounded-lg bg-gray-50"
>
{getFileIcon(file)}
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{file.name}</p>
<p className="text-sm text-gray-500">{formatFileSize(file.size)}</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removeSelectedFile(index)}
disabled={uploading}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
</div>
)}
{/* Uploaded Files */}
{uploadedFiles.length > 0 && showPreview && (
<div className="space-y-2">
<h4 className="font-medium">Uploaded Files ({uploadedFiles.length})</h4>
<div className="grid gap-2">
{uploadedFiles.map((url, index) => (
<div
key={`uploaded-${index}`}
className="flex items-center gap-3 p-3 border rounded-lg bg-green-50"
>
{url.match(/\.(jpg|jpeg|png|gif|webp)$/i) ? (
<div className="relative h-12 w-12 rounded overflow-hidden">
<Image
src={url}
alt="Uploaded file"
fill
className="object-cover"
/>
</div>
) : (
<FileIcon className="h-8 w-8 text-gray-500" />
)}
<div className="flex-1 min-w-0">
<p className="font-medium truncate">
{url.split('/').pop()?.split('?')[0] || 'Uploaded file'}
</p>
<Badge variant="secondary" className="text-xs">
Uploaded
</Badge>
</div>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => window.open(url, '_blank')}
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteUploadedFile(url)}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}

179
components/ui/form.tsx Normal file
View File

@@ -0,0 +1,179 @@
'use client';
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from 'react-hook-form';
import { cn } from '@/lib/utils';
import { Label } from '@/components/ui/label';
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>');
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn('space-y-2', className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = 'FormItem';
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && 'text-destructive', className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = 'FormLabel';
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = 'FormControl';
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
);
});
FormDescription.displayName = 'FormDescription';
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn('text-sm font-medium text-destructive', className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = 'FormMessage';
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};

25
components/ui/input.tsx Normal file
View File

@@ -0,0 +1,25 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = 'Input';
export { Input };

26
components/ui/label.tsx Normal file
View File

@@ -0,0 +1,26 @@
'use client';
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

21
components/ui/loading.tsx Normal file
View File

@@ -0,0 +1,21 @@
import { Loader2 } from 'lucide-react'
interface LoadingProps {
size?: 'sm' | 'md' | 'lg'
text?: string
}
export function Loading({ size = 'md', text }: LoadingProps) {
const sizeClasses = {
sm: 'h-4 w-4',
md: 'h-8 w-8',
lg: 'h-12 w-12'
}
return (
<div className="flex items-center justify-center space-x-2">
<Loader2 className={`animate-spin ${sizeClasses[size]}`} />
{text && <span className="text-sm text-muted-foreground">{text}</span>}
</div>
)
}

236
components/ui/menubar.tsx Normal file
View File

@@ -0,0 +1,236 @@
'use client';
import * as React from 'react';
import * as MenubarPrimitive from '@radix-ui/react-menubar';
import { Check, ChevronRight, Circle } from 'lucide-react';
import { cn } from '@/lib/utils';
const MenubarMenu = MenubarPrimitive.Menu;
const MenubarGroup = MenubarPrimitive.Group;
const MenubarPortal = MenubarPrimitive.Portal;
const MenubarSub = MenubarPrimitive.Sub;
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
'flex h-10 items-center space-x-1 rounded-md border bg-background p-1',
className
)}
{...props}
/>
));
Menubar.displayName = MenubarPrimitive.Root.displayName;
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
className
)}
{...props}
/>
));
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
));
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
));
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
(
{ className, align = 'start', alignOffset = -4, sideOffset = 8, ...props },
ref
) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
</MenubarPrimitive.Portal>
)
);
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
className
)}
{...props}
/>
));
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
));
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
));
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className
)}
{...props}
/>
));
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
const MenubarShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
'ml-auto text-xs tracking-widest text-muted-foreground',
className
)}
{...props}
/>
);
};
MenubarShortcut.displayname = 'MenubarShortcut';
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
};

View File

@@ -0,0 +1,128 @@
import * as React from 'react';
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
import { cva } from 'class-variance-authority';
import { ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
'relative z-10 flex max-w-max flex-1 items-center justify-center',
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
'group flex flex-1 list-none items-center justify-center space-x-1',
className
)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
'group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50'
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), 'group', className)}
{...props}
>
{children}{' '}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
'left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ',
className
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn('absolute left-0 top-full flex justify-center')}>
<NavigationMenuPrimitive.Viewport
className={cn(
'origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]',
className
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
'top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in',
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};

View File

@@ -0,0 +1,101 @@
'use client'
import { useEffect, useState } from 'react'
import { motion } from 'framer-motion'
import Image from 'next/image'
interface PageLoaderProps {
onLoadingComplete: () => void
duration?: number
}
export default function PageLoader({ onLoadingComplete, duration = 800 }: PageLoaderProps) {
const [progress, setProgress] = useState(0)
useEffect(() => {
// Progress bar animation - faster completion
const progressInterval = setInterval(() => {
setProgress(prev => {
if (prev >= 100) {
clearInterval(progressInterval)
return 100
}
return prev + 12.5 // Will complete in 0.8 seconds (100/12.5 * 100ms)
})
}, 100)
// Complete loading after duration
const loadingTimeout = setTimeout(() => {
onLoadingComplete()
}, duration)
return () => {
clearInterval(progressInterval)
clearTimeout(loadingTimeout)
}
}, [duration, onLoadingComplete])
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50"
>
<div className="text-center space-y-8 max-w-4xl mx-auto px-6">
{/* Kashmina Logo */}
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="flex justify-center mb-8"
>
<div className="relative w-32 h-32 md:w-40 md:h-40">
<Image
src="/kashmina-logo.png"
alt="Kashmina Logo"
fill
className="object-contain"
priority
sizes="(max-width: 768px) 128px, 160px"
/>
</div>
</motion.div>
{/* Welcome Heading */}
<motion.div
initial={{ y: 30, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="space-y-4"
>
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold bg-gradient-to-r from-amber-700 via-orange-600 to-red-600 bg-clip-text text-transparent leading-tight">
Welcome to Padmaaja Rasooi
</h1>
<p className="text-lg md:text-xl text-slate-600 font-medium">
Premium Rice Authentic Quality From Farm to Kitchen
</p>
</motion.div>
{/* Progress Bar */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4, duration: 0.4 }}
className="w-full max-w-md mx-auto space-y-3"
>
<div className="w-full bg-slate-200 rounded-full h-2 overflow-hidden border border-slate-300/50">
<motion.div
className="h-full bg-gradient-to-r from-amber-500 via-orange-500 to-red-500 rounded-full shadow-sm"
style={{ width: `${Math.round(progress)}%` }}
transition={{ duration: 0.1 }}
/>
</div>
<p className="text-sm text-slate-500 font-medium">
Loading your premium rice experience...
</p>
</motion.div>
</div>
</motion.div>
)
}

View File

@@ -0,0 +1,117 @@
import * as React from 'react';
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
import { cn } from '@/lib/utils';
import { ButtonProps, buttonVariants } from '@/components/ui/button';
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
<nav
role="navigation"
aria-label="pagination"
className={cn('mx-auto flex w-full justify-center', className)}
{...props}
/>
);
Pagination.displayName = 'Pagination';
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn('flex flex-row items-center gap-1', className)}
{...props}
/>
));
PaginationContent.displayName = 'PaginationContent';
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<'li'>
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn('', className)} {...props} />
));
PaginationItem.displayName = 'PaginationItem';
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, 'size'> &
React.ComponentProps<'a'>;
const PaginationLink = ({
className,
isActive,
size = 'icon',
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? 'page' : undefined}
className={cn(
buttonVariants({
variant: isActive ? 'outline' : 'ghost',
size,
}),
className
)}
{...props}
/>
);
PaginationLink.displayName = 'PaginationLink';
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn('gap-1 pl-2.5', className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = 'PaginationPrevious';
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn('gap-1 pr-2.5', className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
PaginationNext.displayName = 'PaginationNext';
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<'span'>) => (
<span
aria-hidden
className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = 'PaginationEllipsis';
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};

View File

@@ -0,0 +1,28 @@
'use client';
import * as React from 'react';
import * as ProgressPrimitive from '@radix-ui/react-progress';
import { cn } from '@/lib/utils';
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
'relative h-4 w-full overflow-hidden rounded-full bg-secondary',
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };

View File

@@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

160
components/ui/select.tsx Normal file
View File

@@ -0,0 +1,160 @@
'use client';
import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select';
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
import { cn } from '@/lib/utils';
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1',
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1',
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};

View File

@@ -0,0 +1,31 @@
'use client';
import * as React from 'react';
import * as SeparatorPrimitive from '@radix-ui/react-separator';
import { cn } from '@/lib/utils';
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = 'horizontal', decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
'shrink-0 bg-border',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className
)}
{...props}
/>
)
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };

140
components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,140 @@
'use client';
import * as React from 'react';
import * as SheetPrimitive from '@radix-ui/react-dialog';
import { cva, type VariantProps } from 'class-variance-authority';
import { X } from 'lucide-react';
import { cn } from '@/lib/utils';
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom:
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right:
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
},
},
defaultVariants: {
side: 'right',
},
}
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = 'right', className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className
)}
{...props}
/>
);
SheetHeader.displayName = 'SheetHeader';
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
);
SheetFooter.displayName = 'SheetFooter';
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-foreground', className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};

View File

@@ -0,0 +1,15 @@
import { cn } from '@/lib/utils';
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn('animate-pulse rounded-md bg-muted', className)}
{...props}
/>
);
}
export { Skeleton };

31
components/ui/sonner.tsx Normal file
View File

@@ -0,0 +1,31 @@
'use client';
import { useTheme } from 'next-themes';
import { Toaster as Sonner } from 'sonner';
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = 'system' } = useTheme();
return (
<Sonner
theme={theme as ToasterProps['theme']}
className="toaster group"
toastOptions={{
classNames: {
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton:
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton:
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
},
}}
{...props}
/>
);
};
export { Toaster };

29
components/ui/switch.tsx Normal file
View File

@@ -0,0 +1,29 @@
'use client';
import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cn } from '@/lib/utils';
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };

117
components/ui/table.tsx Normal file
View File

@@ -0,0 +1,117 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn('w-full caption-bottom text-sm', className)}
{...props}
/>
</div>
));
Table.displayName = 'Table';
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
));
TableHeader.displayName = 'TableHeader';
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn('[&_tr:last-child]:border-0', className)}
{...props}
/>
));
TableBody.displayName = 'TableBody';
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
className
)}
{...props}
/>
));
TableFooter.displayName = 'TableFooter';
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
className
)}
{...props}
/>
));
TableRow.displayName = 'TableRow';
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
className
)}
{...props}
/>
));
TableHead.displayName = 'TableHead';
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
{...props}
/>
));
TableCell.displayName = 'TableCell';
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn('mt-4 text-sm text-muted-foreground', className)}
{...props}
/>
));
TableCaption.displayName = 'TableCaption';
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

55
components/ui/tabs.tsx Normal file
View File

@@ -0,0 +1,55 @@
'use client';
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '@/lib/utils';
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -0,0 +1,24 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
);
}
);
Textarea.displayName = 'Textarea';
export { Textarea };

129
components/ui/toast.tsx Normal file
View File

@@ -0,0 +1,129 @@
'use client';
import * as React from 'react';
import * as ToastPrimitives from '@radix-ui/react-toast';
import { cva, type VariantProps } from 'class-variance-authority';
import { X } from 'lucide-react';
import { cn } from '@/lib/utils';
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
{
variants: {
variant: {
default: 'border bg-background text-foreground',
destructive:
'destructive group border-destructive bg-destructive text-destructive-foreground',
},
},
defaultVariants: {
variant: 'default',
},
}
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold', className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn('text-sm opacity-90', className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

35
components/ui/toaster.tsx Normal file
View File

@@ -0,0 +1,35 @@
'use client';
import { useToast } from '@/hooks/use-toast';
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from '@/components/ui/toast';
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

45
components/ui/toggle.tsx Normal file
View File

@@ -0,0 +1,45 @@
'use client';
import * as React from 'react';
import * as TogglePrimitive from '@radix-ui/react-toggle';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const toggleVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
{
variants: {
variant: {
default: 'bg-transparent',
outline:
'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-10 px-3',
sm: 'h-9 px-2.5',
lg: 'h-11 px-5',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
));
Toggle.displayName = TogglePrimitive.Root.displayName;
export { Toggle, toggleVariants };

30
components/ui/tooltip.tsx Normal file
View File

@@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }