);
};
export default ProductForm;
================================================
FILE: components/deal-countdown.tsx
================================================
'use client';
import Link from 'next/link';
import { Button } from './ui/button';
import Image from 'next/image';
import { useEffect, useState } from 'react';
// Static target date (replace with desired date)
const TARGET_DATE = new Date('2025-01-20T00:00:00');
// Function to calculate the time remaining
const calculateTimeRemaining = (targetDate: Date) => {
const currentTime = new Date();
const timeDifference = Math.max(Number(targetDate) - Number(currentTime), 0);
return {
days: Math.floor(timeDifference / (1000 * 60 * 60 * 24)),
hours: Math.floor(
(timeDifference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
),
minutes: Math.floor((timeDifference % (1000 * 60 * 60)) / (1000 * 60)),
seconds: Math.floor((timeDifference % (1000 * 60)) / 1000),
};
};
const DealCountdown = () => {
const [time, setTime] = useState>();
useEffect(() => {
// Calculate initial time on client
setTime(calculateTimeRemaining(TARGET_DATE));
const timerInterval = setInterval(() => {
const newTime = calculateTimeRemaining(TARGET_DATE);
setTime(newTime);
if (
newTime.days === 0 &&
newTime.hours === 0 &&
newTime.minutes === 0 &&
newTime.seconds === 0
) {
clearInterval(timerInterval);
}
return () => clearInterval(timerInterval);
}, 1000);
}, []);
if (!time) {
return (
);
}
if (
time.days === 0 &&
time.hours === 0 &&
time.minutes === 0 &&
time.seconds === 0
) {
return (
Deal Has Ended
This deal is no longer available. Check out our latest promotions!
View Products
);
}
return (
Deal Of The Month
Get ready for a shopping experience like never before with our Deals
of the Month! Every purchase comes with exclusive perks and offers,
making this month a celebration of savvy choices and amazing deals.
Don't miss out! 🎁🛒
View Products
);
};
const StatBox = ({ label, value }: { label: string; value: number }) => (
{value}
{label}
);
export default DealCountdown;
================================================
FILE: components/footer.tsx
================================================
import { APP_NAME } from '@/lib/constants';
const Footer = () => {
const currentYear = new Date().getFullYear();
return (
);
};
export default Footer;
================================================
FILE: components/icon-boxes.tsx
================================================
import { DollarSign, Headset, ShoppingBag, WalletCards } from 'lucide-react';
import { Card, CardContent } from './ui/card';
const IconBoxes = () => {
return (
Free Shipping
Free shipping on orders above $100
Money Back Guarantee
Within 30 days of purchase
Flexible Payment
Pay with credit card, PayPal or COD
24/7 Support
Get support at any time
);
};
export default IconBoxes;
================================================
FILE: components/shared/checkout-steps.tsx
================================================
import React from 'react';
import { cn } from '@/lib/utils';
const CheckoutSteps = ({ current = 0 }) => {
return (
{['User Login', 'Shipping Address', 'Payment Method', 'Place Order'].map(
(step, index) => (
{step}
{step !== 'Place Order' && (
)}
)
)}
);
};
export default CheckoutSteps;
================================================
FILE: components/shared/delete-dialog.tsx
================================================
'use client';
import { useState } from 'react';
import { useTransition } from 'react';
import { useToast } from '@/hooks/use-toast';
import { Button } from '../ui/button';
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '../ui/alert-dialog';
const DeleteDialog = ({
id,
action,
}: {
id: string;
action: (id: string) => Promise<{ success: boolean; message: string }>;
}) => {
const [open, setOpen] = useState(false);
const [isPending, startTransition] = useTransition();
const { toast } = useToast();
const handleDeleteClick = () => {
startTransition(async () => {
const res = await action(id);
if (!res.success) {
toast({
variant: 'destructive',
description: res.message,
});
} else {
setOpen(false);
toast({
description: res.message,
});
}
});
};
return (
Delete
Are you absolutely sure?
This action cannot be undone
Cancel
{isPending ? 'Deleting...' : 'Delete'}
);
};
export default DeleteDialog;
================================================
FILE: components/shared/header/category-drawer.tsx
================================================
import { Button } from '@/components/ui/button';
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from '@/components/ui/drawer';
import { getAllCategories } from '@/lib/actions/product.actions';
import { MenuIcon } from 'lucide-react';
import Link from 'next/link';
const CategoryDrawer = async () => {
const categories = await getAllCategories();
return (
Select a category
{categories.map((x) => (
{x.category} ({x._count})
))}
);
};
export default CategoryDrawer;
================================================
FILE: components/shared/header/index.tsx
================================================
import Image from 'next/image';
import Link from 'next/link';
import { APP_NAME } from '@/lib/constants';
import Menu from './menu';
import CategoryDrawer from './category-drawer';
import Search from './search';
const Header = () => {
return (
);
};
export default Header;
================================================
FILE: components/shared/header/menu.tsx
================================================
import { Button } from '@/components/ui/button';
import ModeToggle from './mode-toggle';
import Link from 'next/link';
import { EllipsisVertical, ShoppingCart } from 'lucide-react';
import {
Sheet,
SheetContent,
SheetDescription,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import UserButton from './user-button';
const Menu = () => {
return (
Cart
Menu
Cart
);
};
export default Menu;
================================================
FILE: components/shared/header/mode-toggle.tsx
================================================
'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuContent,
DropdownMenuCheckboxItem,
} from '@/components/ui/dropdown-menu';
import { useTheme } from 'next-themes';
import { SunIcon, MoonIcon, SunMoon } from 'lucide-react';
const ModeToggle = () => {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
{theme === 'system' ? (
) : theme === 'dark' ? (
) : (
)}
Appearance
setTheme('system')}
>
System
setTheme('dark')}
>
Dark
setTheme('light')}
>
Light
);
};
export default ModeToggle;
================================================
FILE: components/shared/header/search.tsx
================================================
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { getAllCategories } from '@/lib/actions/product.actions';
import { SearchIcon } from 'lucide-react';
const Search = async () => {
const categories = await getAllCategories();
return (
All
{categories.map((x) => (
{x.category}
))}
);
};
export default Search;
================================================
FILE: components/shared/header/user-button.tsx
================================================
import Link from 'next/link';
import { auth } from '@/auth';
import { signOutUser } from '@/lib/actions/user.actions';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { UserIcon } from 'lucide-react';
const UserButton = async () => {
const session = await auth();
if (!session) {
return (
Sign In
);
}
const firstInitial = session.user?.name?.charAt(0).toUpperCase() ?? 'U';
return (
{firstInitial}
{session.user?.name}
{session.user?.email}
User Profile
Order History
{session?.user?.role === 'admin' && (
Admin
)}
Sign Out
);
};
export default UserButton;
================================================
FILE: components/shared/pagination.tsx
================================================
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { Button } from '../ui/button';
import { formUrlQuery } from '@/lib/utils';
type PaginationProps = {
page: number | string;
totalPages: number;
urlParamName?: string;
};
const Pagination = ({ page, totalPages, urlParamName }: PaginationProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const handleClick = (btnType: string) => {
const pageValue = btnType === 'next' ? Number(page) + 1 : Number(page) - 1;
const newUrl = formUrlQuery({
params: searchParams.toString(),
key: urlParamName || 'page',
value: pageValue.toString(),
});
router.push(newUrl);
};
return (
handleClick('prev')}
>
Previous
= totalPages}
onClick={() => handleClick('next')}
>
Next
);
};
export default Pagination;
================================================
FILE: components/shared/product/add-to-cart.tsx
================================================
'use client';
import { Button } from '@/components/ui/button';
import { useRouter } from 'next/navigation';
import { Plus, Minus, Loader } from 'lucide-react';
import { Cart, CartItem } from '@/types';
import { useToast } from '@/hooks/use-toast';
import { ToastAction } from '@/components/ui/toast';
import { addItemToCart, removeItemFromCart } from '@/lib/actions/cart.actions';
import { useTransition } from 'react';
const AddToCart = ({ cart, item }: { cart?: Cart; item: CartItem }) => {
const router = useRouter();
const { toast } = useToast();
const [isPending, startTransition] = useTransition();
const handleAddToCart = async () => {
startTransition(async () => {
const res = await addItemToCart(item);
if (!res.success) {
toast({
variant: 'destructive',
description: res.message,
});
return;
}
// Handle success add to cart
toast({
description: res.message,
action: (
router.push('/cart')}
>
Go To Cart
),
});
});
};
// Handle remove from cart
const handleRemoveFromCart = async () => {
startTransition(async () => {
const res = await removeItemFromCart(item.productId);
toast({
variant: res.success ? 'default' : 'destructive',
description: res.message,
});
return;
});
};
// Check if item is in cart
const existItem =
cart && cart.items.find((x) => x.productId === item.productId);
return existItem ? (
{isPending ? (
) : (
)}
{existItem.qty}
{isPending ? (
) : (
)}
) : (
{isPending ? (
) : (
)}{' '}
Add To Cart
);
};
export default AddToCart;
================================================
FILE: components/shared/product/product-card.tsx
================================================
import Link from 'next/link';
import Image from 'next/image';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import ProductPrice from './product-price';
import { Product } from '@/types';
import Rating from './rating';
const ProductCard = ({ product }: { product: Product }) => {
return (
{product.brand}
{product.name}
{product.stock > 0 ? (
) : (
Out Of Stock
)}
);
};
export default ProductCard;
================================================
FILE: components/shared/product/product-carousel.tsx
================================================
'use client';
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from '@/components/ui/carousel';
import { Product } from '@/types';
import Autoplay from 'embla-carousel-autoplay';
import Link from 'next/link';
import Image from 'next/image';
const ProductCarousel = ({ data }: { data: Product[] }) => {
return (
{data.map((product: Product) => (
))}
);
};
export default ProductCarousel;
================================================
FILE: components/shared/product/product-images.tsx
================================================
'use client';
import { useState } from 'react';
import Image from 'next/image';
import { cn } from '@/lib/utils';
const ProductImages = ({ images }: { images: string[] }) => {
const [current, setCurrent] = useState(0);
return (
{images.map((image, index) => (
setCurrent(index)}
className={cn(
'border mr-2 cursor-pointer hover:border-orange-600',
current === index && 'border-orange-500'
)}
>
))}
);
};
export default ProductImages;
================================================
FILE: components/shared/product/product-list.tsx
================================================
import ProductCard from './product-card';
import { Product } from '@/types';
const ProductList = ({
data,
title,
limit,
}: {
data: Product[];
title?: string;
limit?: number;
}) => {
const limitedData = limit ? data.slice(0, limit) : data;
return (
{title}
{data.length > 0 ? (
{limitedData.map((product: Product) => (
))}
) : (
)}
);
};
export default ProductList;
================================================
FILE: components/shared/product/product-price.tsx
================================================
import { cn } from '@/lib/utils';
const ProductPrice = ({
value,
className,
}: {
value: number;
className?: string;
}) => {
// Ensure two decimal places
const stringValue = value.toFixed(2);
// Get the int/float
const [intValue, floatValue] = stringValue.split('.');
return (
$
{intValue}
.{floatValue}
);
};
export default ProductPrice;
================================================
FILE: components/shared/product/rating.tsx
================================================
const Rating = ({ value, caption }: { value: number; caption?: string }) => {
const Full = () => (
);
const Half = () => (
);
const Empty = () => (
);
return (
{value >= 1 ? : value >= 0.5 ? : }
{value >= 2 ? : value >= 1.5 ? : }
{value >= 3 ? : value >= 2.5 ? : }
{value >= 4 ? : value >= 3.5 ? : }
{value >= 5 ? : value >= 4.5 ? : }
{caption &&
{caption} }
);
};
export default Rating;
================================================
FILE: components/ui/alert-dialog.tsx
================================================
"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,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes) => (
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes) => (
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
================================================
FILE: components/ui/badge.tsx
================================================
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,
VariantProps {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
)
}
export { Badge, badgeVariants }
================================================
FILE: components/ui/button.tsx
================================================
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,
VariantProps {
asChild?: boolean
}
const Button = React.forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
================================================
FILE: components/ui/card.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
================================================
FILE: components/ui/carousel.tsx
================================================
"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
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[0]
api: ReturnType[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a ")
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes & 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) => {
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 (
{children}
)
}
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
)
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
)
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
Previous slide
)
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
Next slide
)
})
CarouselNext.displayName = "CarouselNext"
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}
================================================
FILE: components/ui/checkbox.tsx
================================================
"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,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }
================================================
FILE: components/ui/dialog.tsx
================================================
"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,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
Close
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes) => (
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes) => (
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
================================================
FILE: components/ui/drawer.tsx
================================================
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps) => (
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes) => (
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes) => (
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
================================================
FILE: components/ui/dropdown-menu.tsx
================================================
"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,
React.ComponentPropsWithoutRef & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
{children}
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, sideOffset = 4, ...props }, ref) => (
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, checked, ...props }, ref) => (
{children}
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes) => {
return (
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
================================================
FILE: components/ui/form.tsx
================================================
"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 = FieldPath
> = {
name: TName
}
const FormFieldContext = React.createContext(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath = FieldPath
>({
...props
}: ControllerProps) => {
return (
)
}
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 ")
}
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(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
{body}
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}
================================================
FILE: components/ui/input.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef>(
({ className, type, ...props }, ref) => {
return (
)
}
)
Input.displayName = "Input"
export { Input }
================================================
FILE: components/ui/label.tsx
================================================
"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,
React.ComponentPropsWithoutRef &
VariantProps
>(({ className, ...props }, ref) => (
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
================================================
FILE: components/ui/radio-group.tsx
================================================
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => {
return (
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => {
return (
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }
================================================
FILE: components/ui/select.tsx
================================================
"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,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
span]:line-clamp-1",
className
)}
{...props}
>
{children}
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, position = "popper", ...props }, ref) => (
{children}
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
================================================
FILE: components/ui/sheet.tsx
================================================
"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,
React.ComponentPropsWithoutRef
>(({ className, ...props }, 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,
VariantProps {}
const SheetContent = React.forwardRef<
React.ElementRef,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
{children}
Close
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes) => (
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes) => (
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
================================================
FILE: components/ui/table.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes
>(({ className, ...props }, ref) => (
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes
>(({ className, ...props }, ref) => (
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
================================================
FILE: components/ui/textarea.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
)
})
Textarea.displayName = "Textarea"
export { Textarea }
================================================
FILE: components/ui/toast.tsx
================================================
"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,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
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,
React.ComponentPropsWithoutRef &
VariantProps
>(({ className, variant, ...props }, ref) => {
return (
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef
type ToastActionElement = React.ReactElement
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
================================================
FILE: components/ui/toaster.tsx
================================================
"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 (
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
{title && {title} }
{description && (
{description}
)}
{action}
)
})}
)
}
================================================
FILE: components/view-all-products-button.tsx
================================================
import { Button } from './ui/button';
import Link from 'next/link';
const ViewAllProductsButton = () => {
return (
View All Products
);
};
export default ViewAllProductsButton;
================================================
FILE: components.json
================================================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "assets/styles/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
================================================
FILE: db/prisma.ts
================================================
import { Pool, neonConfig } from '@neondatabase/serverless';
import { PrismaNeon } from '@prisma/adapter-neon';
import { PrismaClient } from '@prisma/client';
import ws from 'ws';
// Sets up WebSocket connections, which enables Neon to use WebSocket communication.
neonConfig.webSocketConstructor = ws;
const connectionString = `${process.env.DATABASE_URL}`;
// Creates a new connection pool using the provided connection string, allowing multiple concurrent connections.
const pool = new Pool({ connectionString });
// Instantiates the Prisma adapter using the Neon connection pool to handle the connection between Prisma and Neon.
const adapter = new PrismaNeon(pool);
// Extends the PrismaClient with a custom result transformer to convert the price and rating fields to strings.
export const prisma = new PrismaClient({ adapter }).$extends({
result: {
product: {
price: {
compute(product) {
return product.price.toString();
},
},
rating: {
compute(product) {
return product.rating.toString();
},
},
},
cart: {
itemsPrice: {
needs: { itemsPrice: true },
compute(cart) {
return cart.itemsPrice.toString();
},
},
shippingPrice: {
needs: { shippingPrice: true },
compute(cart) {
return cart.shippingPrice.toString();
},
},
taxPrice: {
needs: { taxPrice: true },
compute(cart) {
return cart.taxPrice.toString();
},
},
totalPrice: {
needs: { totalPrice: true },
compute(cart) {
return cart.totalPrice.toString();
},
},
},
order: {
itemsPrice: {
needs: { itemsPrice: true },
compute(cart) {
return cart.itemsPrice.toString();
},
},
shippingPrice: {
needs: { shippingPrice: true },
compute(cart) {
return cart.shippingPrice.toString();
},
},
taxPrice: {
needs: { taxPrice: true },
compute(cart) {
return cart.taxPrice.toString();
},
},
totalPrice: {
needs: { totalPrice: true },
compute(cart) {
return cart.totalPrice.toString();
},
},
},
orderItem: {
price: {
compute(cart) {
return cart.price.toString();
},
},
},
},
});
================================================
FILE: db/sample-data.ts
================================================
const sampleData = {
users: [
{
name: 'John',
email: 'admin@example.com',
password: '123456',
role: 'admin',
},
{
name: 'Jane',
email: 'user@example.com',
password: '123456',
role: 'user',
},
],
products: [
{
name: 'Polo Sporting Stretch Shirt',
slug: 'polo-sporting-stretch-shirt',
category: "Men's Dress Shirts",
description: 'Classic Polo style with modern comfort',
images: [
'/images/sample-products/p1-1.jpg',
'/images/sample-products/p1-2.jpg',
],
price: 59.99,
brand: 'Polo',
rating: 4.5,
numReviews: 10,
stock: 5,
isFeatured: true,
banner: '/images/banner-1.jpg',
},
{
name: 'Brooks Brothers Long Sleeved Shirt',
slug: 'brooks-brothers-long-sleeved-shirt',
category: "Men's Dress Shirts",
description: 'Timeless style and premium comfort',
images: [
'/images/sample-products/p2-1.jpg',
'/images/sample-products/p2-2.jpg',
],
price: 85.9,
brand: 'Brooks Brothers',
rating: 4.2,
numReviews: 8,
stock: 10,
isFeatured: true,
banner: '/images/banner-2.jpg',
},
{
name: 'Tommy Hilfiger Classic Fit Dress Shirt',
slug: 'tommy-hilfiger-classic-fit-dress-shirt',
category: "Men's Dress Shirts",
description: 'A perfect blend of sophistication and comfort',
images: [
'/images/sample-products/p3-1.jpg',
'/images/sample-products/p3-2.jpg',
],
price: 99.95,
brand: 'Tommy Hilfiger',
rating: 4.9,
numReviews: 3,
stock: 0,
isFeatured: false,
banner: null,
},
{
name: 'Calvin Klein Slim Fit Stretch Shirt',
slug: 'calvin-klein-slim-fit-stretch-shirt',
category: "Men's Dress Shirts",
description: 'Streamlined design with flexible stretch fabric',
images: [
'/images/sample-products/p4-1.jpg',
'/images/sample-products/p4-2.jpg',
],
price: 39.95,
brand: 'Calvin Klein',
rating: 3.6,
numReviews: 5,
stock: 10,
isFeatured: false,
banner: null,
},
{
name: 'Polo Ralph Lauren Oxford Shirt',
slug: 'polo-ralph-lauren-oxford-shirt',
category: "Men's Dress Shirts",
description: 'Iconic Polo design with refined oxford fabric',
images: [
'/images/sample-products/p5-1.jpg',
'/images/sample-products/p5-2.jpg',
],
price: 79.99,
brand: 'Polo',
rating: 4.7,
numReviews: 18,
stock: 6,
isFeatured: false,
banner: null,
},
{
name: 'Polo Classic Pink Hoodie',
slug: 'polo-classic-pink-hoodie',
category: "Men's Sweatshirts",
description: 'Soft, stylish, and perfect for laid-back days',
images: [
'/images/sample-products/p6-1.jpg',
'/images/sample-products/p6-2.jpg',
],
price: 99.99,
brand: 'Polo',
rating: 4.6,
numReviews: 12,
stock: 8,
isFeatured: true,
banner: null,
},
],
};
export default sampleData;
================================================
FILE: db/seed.ts
================================================
import { PrismaClient } from '@prisma/client';
import sampleData from './sample-data';
import { hash } from '@/lib/encrypt';
async function main() {
const prisma = new PrismaClient();
await prisma.product.deleteMany();
await prisma.account.deleteMany();
await prisma.session.deleteMany();
await prisma.verificationToken.deleteMany();
await prisma.user.deleteMany();
await prisma.product.createMany({ data: sampleData.products });
const users = [];
for (let i = 0; i < sampleData.users.length; i++) {
users.push({
...sampleData.users[i],
password: await hash(sampleData.users[i].password),
});
console.log(
sampleData.users[i].password,
await hash(sampleData.users[i].password)
);
}
await prisma.user.createMany({ data: users });
console.log('Database seeded successfully!');
}
main();
================================================
FILE: email/index.tsx
================================================
import { Resend } from 'resend';
import { SENDER_EMAIL, APP_NAME } from '@/lib/constants';
import { Order } from '@/types';
import dotenv from 'dotenv';
dotenv.config();
import PurchaseReceiptEmail from './purchase-receipt';
const resend = new Resend(process.env.RESEND_API_KEY as string);
export const sendPurchaseReceipt = async ({ order }: { order: Order }) => {
await resend.emails.send({
from: `${APP_NAME} <${SENDER_EMAIL}>`,
to: order.user.email,
subject: `Order Confirmation ${order.id}`,
react: ,
});
};
================================================
FILE: email/purchase-receipt.tsx
================================================
import {
Body,
Column,
Container,
Head,
Heading,
Html,
Img,
Preview,
Row,
Section,
Tailwind,
Text,
} from '@react-email/components';
import { Order } from '@/types';
import { formatCurrency } from '@/lib/utils';
import sampleData from '@/db/sample-data';
require('dotenv').config();
PurchaseReceiptEmail.PreviewProps = {
order: {
id: crypto.randomUUID(),
userId: '123',
user: {
name: 'John Doe',
email: 'test@test.com',
},
paymentMethod: 'Stripe',
shippingAddress: {
fullName: 'John Doe',
streetAddress: '123 Main st',
city: 'New York',
postalCode: '10001',
country: 'US',
},
createdAt: new Date(),
totalPrice: '100',
taxPrice: '10',
shippingPrice: '10',
itemsPrice: '80',
orderitems: sampleData.products.map((x) => ({
name: x.name,
orderId: '123',
productId: '123',
slug: x.slug,
qty: x.stock,
image: x.images[0],
price: x.price.toString(),
})),
isDelivered: true,
deliveredAt: new Date(),
isPaid: true,
paidAt: new Date(),
paymentResult: {
id: '123',
status: 'succeeded',
pricePaid: '100',
email_address: 'test@test.com',
},
},
} satisfies OrderInformationProps;
const dateFormatter = new Intl.DateTimeFormat('en', { dateStyle: 'medium' });
type OrderInformationProps = {
order: Order;
};
export default function PurchaseReceiptEmail({ order }: OrderInformationProps) {
return (
View order receipt
Purchase Receipt
Order ID
{order.id.toString()}
Purchase Date
{dateFormatter.format(order.createdAt)}
Price Paid
{formatCurrency(order.totalPrice)}
{order.orderitems.map((item) => (
{item.name} x {item.qty}
{formatCurrency(item.price)}
))}
{[
{ name: 'Items', price: order.itemsPrice },
{ name: 'Tax', price: order.taxPrice },
{ name: 'Shipping', price: order.shippingPrice },
{ name: 'Total', price: order.totalPrice },
].map(({ name, price }) => (
{name}:
{formatCurrency(price)}
))}
);
}
================================================
FILE: hooks/use-toast.ts
================================================
"use client"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }
================================================
FILE: jest.config.ts
================================================
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
import type { Config } from 'jest';
const config: Config = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/39/tkk6884x1ydgmbvfshhs67vr0000gn/T/jest_dx",
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
// coverageDirectory: undefined,
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: 'v8',
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
preset: 'ts-jest',
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// ""
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
setupFiles: ['/jest.setup.ts'],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
export default config;
================================================
FILE: jest.setup.ts
================================================
require('dotenv').config();
================================================
FILE: lib/actions/cart.actions.ts
================================================
'use server';
import { cookies } from 'next/headers';
import { CartItem } from '@/types';
import { convertToPlainObject, formatError, round2 } from '../utils';
import { auth } from '@/auth';
import { prisma } from '@/db/prisma';
import { cartItemSchema, insertCartSchema } from '../validators';
import { revalidatePath } from 'next/cache';
import { Prisma } from '@prisma/client';
// Calculate cart prices
const calcPrice = (items: CartItem[]) => {
const itemsPrice = round2(
items.reduce((acc, item) => acc + Number(item.price) * item.qty, 0)
),
shippingPrice = round2(itemsPrice > 100 ? 0 : 10),
taxPrice = round2(0.15 * itemsPrice),
totalPrice = round2(itemsPrice + taxPrice + shippingPrice);
return {
itemsPrice: itemsPrice.toFixed(2),
shippingPrice: shippingPrice.toFixed(2),
taxPrice: taxPrice.toFixed(2),
totalPrice: totalPrice.toFixed(2),
};
};
export async function addItemToCart(data: CartItem) {
try {
// Check for cart cookie
const sessionCartId = (await cookies()).get('sessionCartId')?.value;
if (!sessionCartId) throw new Error('Cart session not found');
// Get session and user ID
const session = await auth();
const userId = session?.user?.id ? (session.user.id as string) : undefined;
// Get cart
const cart = await getMyCart();
// Parse and validate item
const item = cartItemSchema.parse(data);
// Find product in database
const product = await prisma.product.findFirst({
where: { id: item.productId },
});
if (!product) throw new Error('Product not found');
if (!cart) {
// Create new cart object
const newCart = insertCartSchema.parse({
userId: userId,
items: [item],
sessionCartId: sessionCartId,
...calcPrice([item]),
});
// Add to database
await prisma.cart.create({
data: newCart,
});
// Revalidate product page
revalidatePath(`/product/${product.slug}`);
return {
success: true,
message: `${product.name} added to cart`,
};
} else {
// Check if item is already in cart
const existItem = (cart.items as CartItem[]).find(
(x) => x.productId === item.productId
);
if (existItem) {
// Check stock
if (product.stock < existItem.qty + 1) {
throw new Error('Not enough stock');
}
// Increase the quantity
(cart.items as CartItem[]).find(
(x) => x.productId === item.productId
)!.qty = existItem.qty + 1;
} else {
// If item does not exist in cart
// Check stock
if (product.stock < 1) throw new Error('Not enough stock');
// Add item to the cart.items
cart.items.push(item);
}
// Save to database
await prisma.cart.update({
where: { id: cart.id },
data: {
items: cart.items as Prisma.CartUpdateitemsInput[],
...calcPrice(cart.items as CartItem[]),
},
});
revalidatePath(`/product/${product.slug}`);
return {
success: true,
message: `${product.name} ${
existItem ? 'updated in' : 'added to'
} cart`,
};
}
} catch (error) {
return {
success: false,
message: formatError(error),
};
}
}
export async function getMyCart() {
// Check for cart cookie
const sessionCartId = (await cookies()).get('sessionCartId')?.value;
if (!sessionCartId) throw new Error('Cart session not found');
// Get session and user ID
const session = await auth();
const userId = session?.user?.id ? (session.user.id as string) : undefined;
// Get user cart from database
const cart = await prisma.cart.findFirst({
where: userId ? { userId: userId } : { sessionCartId: sessionCartId },
});
if (!cart) return undefined;
// Convert decimals and return
return convertToPlainObject({
...cart,
items: cart.items as CartItem[],
itemsPrice: cart.itemsPrice.toString(),
totalPrice: cart.totalPrice.toString(),
shippingPrice: cart.shippingPrice.toString(),
taxPrice: cart.taxPrice.toString(),
});
}
export async function removeItemFromCart(productId: string) {
try {
// Check for cart cookie
const sessionCartId = (await cookies()).get('sessionCartId')?.value;
if (!sessionCartId) throw new Error('Cart session not found');
// Get Product
const product = await prisma.product.findFirst({
where: { id: productId },
});
if (!product) throw new Error('Product not found');
// Get user cart
const cart = await getMyCart();
if (!cart) throw new Error('Cart not found');
// Check for item
const exist = (cart.items as CartItem[]).find(
(x) => x.productId === productId
);
if (!exist) throw new Error('Item not found');
// Check if only one in qty
if (exist.qty === 1) {
// Remove from cart
cart.items = (cart.items as CartItem[]).filter(
(x) => x.productId !== exist.productId
);
} else {
// Decrease qty
(cart.items as CartItem[]).find((x) => x.productId === productId)!.qty =
exist.qty - 1;
}
// Update cart in database
await prisma.cart.update({
where: { id: cart.id },
data: {
items: cart.items as Prisma.CartUpdateitemsInput[],
...calcPrice(cart.items as CartItem[]),
},
});
revalidatePath(`/product/${product.slug}`);
return {
success: true,
message: `${product.name} was removed from cart`,
};
} catch (error) {
return { success: false, message: formatError(error) };
}
}
================================================
FILE: lib/actions/order.actions.ts
================================================
'use server';
import { isRedirectError } from 'next/dist/client/components/redirect';
import { convertToPlainObject, formatError } from '../utils';
import { auth } from '@/auth';
import { getMyCart } from './cart.actions';
import { getUserById } from './user.actions';
import { insertOrderSchema } from '../validators';
import { prisma } from '@/db/prisma';
import { CartItem, PaymentResult, ShippingAddress } from '@/types';
import { paypal } from '../paypal';
import { revalidatePath } from 'next/cache';
import { PAGE_SIZE } from '../constants';
import { Prisma } from '@prisma/client';
import { sendPurchaseReceipt } from '@/email';
// Create order and create the order items
export async function createOrder() {
try {
const session = await auth();
if (!session) throw new Error('User is not authenticated');
const cart = await getMyCart();
const userId = session?.user?.id;
if (!userId) throw new Error('User not found');
const user = await getUserById(userId);
if (!cart || cart.items.length === 0) {
return {
success: false,
message: 'Your cart is empty',
redirectTo: '/cart',
};
}
if (!user.address) {
return {
success: false,
message: 'No shipping address',
redirectTo: '/shipping-address',
};
}
if (!user.paymentMethod) {
return {
success: false,
message: 'No payment method',
redirectTo: '/payment-method',
};
}
// Create order object
const order = insertOrderSchema.parse({
userId: user.id,
shippingAddress: user.address,
paymentMethod: user.paymentMethod,
itemsPrice: cart.itemsPrice,
shippingPrice: cart.shippingPrice,
taxPrice: cart.taxPrice,
totalPrice: cart.totalPrice,
});
// Create a transaction to create order and order items in database
const insertedOrderId = await prisma.$transaction(async (tx) => {
// Create order
const insertedOrder = await tx.order.create({ data: order });
// Create order items from the cart items
for (const item of cart.items as CartItem[]) {
await tx.orderItem.create({
data: {
...item,
price: item.price,
orderId: insertedOrder.id,
},
});
}
// Clear cart
await tx.cart.update({
where: { id: cart.id },
data: {
items: [],
totalPrice: 0,
taxPrice: 0,
shippingPrice: 0,
itemsPrice: 0,
},
});
return insertedOrder.id;
});
if (!insertedOrderId) throw new Error('Order not created');
return {
success: true,
message: 'Order created',
redirectTo: `/order/${insertedOrderId}`,
};
} catch (error) {
if (isRedirectError(error)) throw error;
return { success: false, message: formatError(error) };
}
}
// Get order by id
export async function getOrderById(orderId: string) {
const data = await prisma.order.findFirst({
where: {
id: orderId,
},
include: {
orderitems: true,
user: { select: { name: true, email: true } },
},
});
return convertToPlainObject(data);
}
// Create new paypal order
export async function createPayPalOrder(orderId: string) {
try {
// Get order from database
const order = await prisma.order.findFirst({
where: {
id: orderId,
},
});
if (order) {
// Create paypal order
const paypalOrder = await paypal.createOrder(Number(order.totalPrice));
// Update order with paypal order id
await prisma.order.update({
where: { id: orderId },
data: {
paymentResult: {
id: paypalOrder.id,
email_address: '',
status: '',
pricePaid: 0,
},
},
});
return {
success: true,
message: 'Item order created successfully',
data: paypalOrder.id,
};
} else {
throw new Error('Order not found');
}
} catch (error) {
return { success: false, message: formatError(error) };
}
}
// Approve paypal order and update order to paid
export async function approvePayPalOrder(
orderId: string,
data: { orderID: string }
) {
try {
// Get order from database
const order = await prisma.order.findFirst({
where: {
id: orderId,
},
});
if (!order) throw new Error('Order not found');
const captureData = await paypal.capturePayment(data.orderID);
if (
!captureData ||
captureData.id !== (order.paymentResult as PaymentResult)?.id ||
captureData.status !== 'COMPLETED'
) {
throw new Error('Error in PayPal payment');
}
// Update order to paid
await updateOrderToPaid({
orderId,
paymentResult: {
id: captureData.id,
status: captureData.status,
email_address: captureData.payer.email_address,
pricePaid:
captureData.purchase_units[0]?.payments?.captures[0]?.amount?.value,
},
});
revalidatePath(`/order/${orderId}`);
return {
success: true,
message: 'Your order has been paid',
};
} catch (error) {
return { success: false, message: formatError(error) };
}
}
// Update order to paid
export async function updateOrderToPaid({
orderId,
paymentResult,
}: {
orderId: string;
paymentResult?: PaymentResult;
}) {
// Get order from database
const order = await prisma.order.findFirst({
where: {
id: orderId,
},
include: {
orderitems: true,
},
});
if (!order) throw new Error('Order not found');
if (order.isPaid) throw new Error('Order is already paid');
// Transaction to update order and account for product stock
await prisma.$transaction(async (tx) => {
// Iterate over products and update stock
for (const item of order.orderitems) {
await tx.product.update({
where: { id: item.productId },
data: { stock: { increment: -item.qty } },
});
}
// Set the order to paid
await tx.order.update({
where: { id: orderId },
data: {
isPaid: true,
paidAt: new Date(),
paymentResult,
},
});
});
// Get updated order after transaction
const updatedOrder = await prisma.order.findFirst({
where: { id: orderId },
include: {
orderitems: true,
user: { select: { name: true, email: true } },
},
});
if (!updatedOrder) throw new Error('Order not found');
sendPurchaseReceipt({
order: {
...updatedOrder,
shippingAddress: updatedOrder.shippingAddress as ShippingAddress,
paymentResult: updatedOrder.paymentResult as PaymentResult,
},
});
}
// Get user's orders
export async function getMyOrders({
limit = PAGE_SIZE,
page,
}: {
limit?: number;
page: number;
}) {
const session = await auth();
if (!session) throw new Error('User is not authorized');
const data = await prisma.order.findMany({
where: { userId: session?.user?.id },
orderBy: { createdAt: 'desc' },
take: limit,
skip: (page - 1) * limit,
});
const dataCount = await prisma.order.count({
where: { userId: session?.user?.id },
});
return {
data,
totalPages: Math.ceil(dataCount / limit),
};
}
type SalesDataType = {
month: string;
totalSales: number;
}[];
// Get sales data and order summary
export async function getOrderSummary() {
// Get counts for each resource
const ordersCount = await prisma.order.count();
const productsCount = await prisma.product.count();
const usersCount = await prisma.user.count();
// Calculate the total sales
const totalSales = await prisma.order.aggregate({
_sum: { totalPrice: true },
});
// Get monthly sales
const salesDataRaw = await prisma.$queryRaw<
Array<{ month: string; totalSales: Prisma.Decimal }>
>`SELECT to_char("createdAt", 'MM/YY') as "month", sum("totalPrice") as "totalSales" FROM "Order" GROUP BY to_char("createdAt", 'MM/YY')`;
const salesData: SalesDataType = salesDataRaw.map((entry) => ({
month: entry.month,
totalSales: Number(entry.totalSales),
}));
// Get latest sales
const latestSales = await prisma.order.findMany({
orderBy: { createdAt: 'desc' },
include: {
user: { select: { name: true } },
},
take: 6,
});
return {
ordersCount,
productsCount,
usersCount,
totalSales,
latestSales,
salesData,
};
}
// Get all orders
export async function getAllOrders({
limit = PAGE_SIZE,
page,
query,
}: {
limit?: number;
page: number;
query: string;
}) {
const queryFilter: Prisma.OrderWhereInput =
query && query !== 'all'
? {
user: {
name: {
contains: query,
mode: 'insensitive',
} as Prisma.StringFilter,
},
}
: {};
const data = await prisma.order.findMany({
where: {
...queryFilter,
},
orderBy: { createdAt: 'desc' },
take: limit,
skip: (page - 1) * limit,
include: { user: { select: { name: true } } },
});
const dataCount = await prisma.order.count();
return {
data,
totalPages: Math.ceil(dataCount / limit),
};
}
// Delete an order
export async function deleteOrder(id: string) {
try {
await prisma.order.delete({ where: { id } });
revalidatePath('/admin/orders');
return {
success: true,
message: 'Order deleted successfully',
};
} catch (error) {
return { success: false, message: formatError(error) };
}
}
// Update COD order to paid
export async function updateOrderToPaidCOD(orderId: string) {
try {
await updateOrderToPaid({ orderId });
revalidatePath(`/order/${orderId}`);
return { success: true, message: 'Order marked as paid' };
} catch (error) {
return { success: false, message: formatError(error) };
}
}
// Update COD order to delivered
export async function deliverOrder(orderId: string) {
try {
const order = await prisma.order.findFirst({
where: {
id: orderId,
},
});
if (!order) throw new Error('Order not found');
if (!order.isPaid) throw new Error('Order is not paid');
await prisma.order.update({
where: { id: orderId },
data: {
isDelivered: true,
deliveredAt: new Date(),
},
});
revalidatePath(`/order/${orderId}`);
return {
success: true,
message: 'Order has been marked delivered',
};
} catch (error) {
return { success: false, message: formatError(error) };
}
}
================================================
FILE: lib/actions/product.actions.ts
================================================
'use server';
import { prisma } from '@/db/prisma';
import { convertToPlainObject, formatError } from '../utils';
import { LATEST_PRODUCTS_LIMIT, PAGE_SIZE } from '../constants';
import { revalidatePath } from 'next/cache';
import { insertProductSchema, updateProductSchema } from '../validators';
import { z } from 'zod';
import { Prisma } from '@prisma/client';
// Get latest products
export async function getLatestProducts() {
const data = await prisma.product.findMany({
take: LATEST_PRODUCTS_LIMIT,
orderBy: { createdAt: 'desc' },
});
return convertToPlainObject(data);
}
// Get single product by it's slug
export async function getProductBySlug(slug: string) {
return await prisma.product.findFirst({
where: { slug: slug },
});
}
// Get single product by it's ID
export async function getProductById(productId: string) {
const data = await prisma.product.findFirst({
where: { id: productId },
});
return convertToPlainObject(data);
}
// Get all products
export async function getAllProducts({
query,
limit = PAGE_SIZE,
page,
category,
price,
rating,
sort,
}: {
query: string;
limit?: number;
page: number;
category?: string;
price?: string;
rating?: string;
sort?: string;
}) {
// Query filter
const queryFilter: Prisma.ProductWhereInput =
query && query !== 'all'
? {
name: {
contains: query,
mode: 'insensitive',
} as Prisma.StringFilter,
}
: {};
// Category filter
const categoryFilter = category && category !== 'all' ? { category } : {};
// Price filter
const priceFilter: Prisma.ProductWhereInput =
price && price !== 'all'
? {
price: {
gte: Number(price.split('-')[0]),
lte: Number(price.split('-')[1]),
},
}
: {};
// Rating filter
const ratingFilter =
rating && rating !== 'all'
? {
rating: {
gte: Number(rating),
},
}
: {};
const data = await prisma.product.findMany({
where: {
...queryFilter,
...categoryFilter,
...priceFilter,
...ratingFilter,
},
orderBy:
sort === 'lowest'
? { price: 'asc' }
: sort === 'highest'
? { price: 'desc' }
: sort === 'rating'
? { rating: 'desc' }
: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit,
});
const dataCount = await prisma.product.count();
return {
data,
totalPages: Math.ceil(dataCount / limit),
};
}
// Delete a product
export async function deleteProduct(id: string) {
try {
const productExists = await prisma.product.findFirst({
where: { id },
});
if (!productExists) throw new Error('Product not found');
await prisma.product.delete({ where: { id } });
revalidatePath('/admin/products');
return {
success: true,
message: 'Product deleted successfully',
};
} catch (error) {
return { success: false, message: formatError(error) };
}
}
// Create a product
export async function createProduct(data: z.infer) {
try {
const product = insertProductSchema.parse(data);
await prisma.product.create({ data: product });
revalidatePath('/admin/products');
return {
success: true,
message: 'Product created successfully',
};
} catch (error) {
return { success: false, message: formatError(error) };
}
}
// Update a product
export async function updateProduct(data: z.infer) {
try {
const product = updateProductSchema.parse(data);
const productExists = await prisma.product.findFirst({
where: { id: product.id },
});
if (!productExists) throw new Error('Product not found');
await prisma.product.update({
where: { id: product.id },
data: product,
});
revalidatePath('/admin/products');
return {
success: true,
message: 'Product updated successfully',
};
} catch (error) {
return { success: false, message: formatError(error) };
}
}
// Get all categories
export async function getAllCategories() {
const data = await prisma.product.groupBy({
by: ['category'],
_count: true,
});
return data;
}
// Get featured products
export async function getFeaturedProducts() {
const data = await prisma.product.findMany({
where: { isFeatured: true },
orderBy: { createdAt: 'desc' },
take: 4,
});
return convertToPlainObject(data);
}
================================================
FILE: lib/actions/review.actions.ts
================================================
'use server';
import { z } from 'zod';
import { insertReviewSchema } from '../validators';
import { formatError } from '../utils';
import { auth } from '@/auth';
import { prisma } from '@/db/prisma';
import { revalidatePath } from 'next/cache';
// Create & Update Reviews
export async function createUpdateReview(
data: z.infer
) {
try {
const session = await auth();
if (!session) throw new Error('User is not authenticated');
// Validate and store the review
const review = insertReviewSchema.parse({
...data,
userId: session?.user?.id,
});
// Get product that is being reviewed
const product = await prisma.product.findFirst({
where: { id: review.productId },
});
if (!product) throw new Error('Product not found');
// Check if user already reviewed
const reviewExists = await prisma.review.findFirst({
where: {
productId: review.productId,
userId: review.userId,
},
});
await prisma.$transaction(async (tx) => {
if (reviewExists) {
// Update review
await tx.review.update({
where: { id: reviewExists.id },
data: {
title: review.title,
description: review.description,
rating: review.rating,
},
});
} else {
// Create review
await tx.review.create({ data: review });
}
// Get avg rating
const averageRating = await tx.review.aggregate({
_avg: { rating: true },
where: { productId: review.productId },
});
// Get number of reviews
const numReviews = await tx.review.count({
where: { productId: review.productId },
});
// Update the rating and numReviews in product table
await tx.product.update({
where: { id: review.productId },
data: {
rating: averageRating._avg.rating || 0,
numReviews,
},
});
});
revalidatePath(`/product/${product.slug}`);
return {
success: true,
message: 'Review Updated Successfully',
};
} catch (error) {
return { success: false, message: formatError(error) };
}
}
// Get all reviews for a product
export async function getReviews({ productId }: { productId: string }) {
const data = await prisma.review.findMany({
where: {
productId: productId,
},
include: {
user: {
select: {
name: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
});
return { data };
}
// Get a review written by the current user
export async function getReviewByProductId({
productId,
}: {
productId: string;
}) {
const session = await auth();
if (!session) throw new Error('User is not authenticated');
return await prisma.review.findFirst({
where: {
productId,
userId: session?.user?.id,
},
});
}
================================================
FILE: lib/actions/user.actions.ts
================================================
'use server';
import {
shippingAddressSchema,
signInFormSchema,
signUpFormSchema,
paymentMethodSchema,
updateUserSchema,
} from '../validators';
import { auth, signIn, signOut } from '@/auth';
import { isRedirectError } from 'next/dist/client/components/redirect-error';
import { hash } from '../encrypt';
import { prisma } from '@/db/prisma';
import { formatError } from '../utils';
import { ShippingAddress } from '@/types';
import { z } from 'zod';
import { PAGE_SIZE } from '../constants';
import { revalidatePath } from 'next/cache';
import { Prisma } from '@prisma/client';
import { getMyCart } from './cart.actions';
// Sign in the user with credentials
export async function signInWithCredentials(
prevState: unknown,
formData: FormData
) {
try {
const user = signInFormSchema.parse({
email: formData.get('email'),
password: formData.get('password'),
});
await signIn('credentials', user);
return { success: true, message: 'Signed in successfully' };
} catch (error) {
if (isRedirectError(error)) {
throw error;
}
return { success: false, message: 'Invalid email or password' };
}
}
// Sign user out
export async function signOutUser() {
// get current users cart and delete it so it does not persist to next user
const currentCart = await getMyCart();
if (currentCart?.id) {
await prisma.cart.delete({ where: { id: currentCart.id } });
} else {
console.warn('No cart found for deletion.');
}
await signOut();
}
// Sign up user
export async function signUpUser(prevState: unknown, formData: FormData) {
try {
const user = signUpFormSchema.parse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
confirmPassword: formData.get('confirmPassword'),
});
const plainPassword = user.password;
user.password = await hash(user.password);
await prisma.user.create({
data: {
name: user.name,
email: user.email,
password: user.password,
},
});
await signIn('credentials', {
email: user.email,
password: plainPassword,
});
return { success: true, message: 'User registered successfully' };
} catch (error) {
if (isRedirectError(error)) {
throw error;
}
return { success: false, message: formatError(error) };
}
}
// Get user by the ID
export async function getUserById(userId: string) {
const user = await prisma.user.findFirst({
where: { id: userId },
});
if (!user) throw new Error('User not found');
return user;
}
// Update the user's address
export async function updateUserAddress(data: ShippingAddress) {
try {
const session = await auth();
const currentUser = await prisma.user.findFirst({
where: { id: session?.user?.id },
});
if (!currentUser) throw new Error('User not found');
const address = shippingAddressSchema.parse(data);
await prisma.user.update({
where: { id: currentUser.id },
data: { address },
});
return {
success: true,
message: 'User updated successfully',
};
} catch (error) {
return { success: false, message: formatError(error) };
}
}
// Update user's payment method
export async function updateUserPaymentMethod(
data: z.infer
) {
try {
const session = await auth();
const currentUser = await prisma.user.findFirst({
where: { id: session?.user?.id },
});
if (!currentUser) throw new Error('User not found');
const paymentMethod = paymentMethodSchema.parse(data);
await prisma.user.update({
where: { id: currentUser.id },
data: { paymentMethod: paymentMethod.type },
});
return {
success: true,
message: 'User updated successfully',
};
} catch (error) {
return { success: false, message: formatError(error) };
}
}
// Update the user profile
export async function updateProfile(user: { name: string; email: string }) {
try {
const session = await auth();
const currentUser = await prisma.user.findFirst({
where: {
id: session?.user?.id,
},
});
if (!currentUser) throw new Error('User not found');
await prisma.user.update({
where: {
id: currentUser.id,
},
data: {
name: user.name,
},
});
return {
success: true,
message: 'User updated successfully',
};
} catch (error) {
return { success: false, message: formatError(error) };
}
}
// Get all the users
export async function getAllUsers({
limit = PAGE_SIZE,
page,
query,
}: {
limit?: number;
page: number;
query: string;
}) {
const queryFilter: Prisma.UserWhereInput =
query && query !== 'all'
? {
name: {
contains: query,
mode: 'insensitive',
} as Prisma.StringFilter,
}
: {};
const data = await prisma.user.findMany({
where: {
...queryFilter,
},
orderBy: { createdAt: 'desc' },
take: limit,
skip: (page - 1) * limit,
});
const dataCount = await prisma.user.count();
return {
data,
totalPages: Math.ceil(dataCount / limit),
};
}
// Delete a user
export async function deleteUser(id: string) {
try {
await prisma.user.delete({ where: { id } });
revalidatePath('/admin/users');
return {
success: true,
message: 'User deleted successfully',
};
} catch (error) {
return {
success: false,
message: formatError(error),
};
}
}
// Update a user
export async function updateUser(user: z.infer) {
try {
await prisma.user.update({
where: { id: user.id },
data: {
name: user.name,
role: user.role,
},
});
revalidatePath('/admin/users');
return {
success: true,
message: 'User updated successfully',
};
} catch (error) {
return { success: false, message: formatError(error) };
}
}
================================================
FILE: lib/auth-guard.ts
================================================
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
export async function requireAdmin() {
const session = await auth()
if (session?.user?.role !== 'admin') {
redirect('/unauthorized')
}
return session
}
================================================
FILE: lib/constants/index.ts
================================================
export const APP_NAME = process.env.NEXT_PUBLIC_APP_NAME || 'Prostore';
export const APP_DESCRIPTION =
process.env.NEXT_PUBLIC_APP_DESCRIPTION ||
'A modern ecommerce store built with Next.js';
export const SERVER_URL =
process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000';
export const LATEST_PRODUCTS_LIMIT =
Number(process.env.LATEST_PRODUCTS_LIMIT) || 4;
export const signInDefaultValues = {
email: 'admin@example.com',
password: '123456',
};
export const signUpDefaultValues = {
name: '',
email: '',
password: '',
confirmPassword: '',
};
export const shippingAddressDefaultValues = {
fullName: '',
streetAddress: '',
city: '',
postalCode: '',
country: '',
};
export const PAYMENT_METHODS = process.env.PAYMENT_METHODS
? process.env.PAYMENT_METHODS.split(', ')
: ['PayPal', 'Stripe', 'CashOnDelivery'];
export const DEFAULT_PAYMENT_METHOD =
process.env.DEFAULT_PAYMENT_METHOD || 'PayPal';
export const PAGE_SIZE = Number(process.env.PAGE_SIZE) || 12;
export const productDefaultValues = {
name: '',
slug: '',
category: '',
images: [],
brand: '',
description: '',
price: '0',
stock: 0,
rating: '0',
numReviews: '0',
isFeatured: false,
banner: null,
};
export const USER_ROLES = process.env.USER_ROLES
? process.env.USER_ROLES.split(', ')
: ['admin', 'user'];
export const reviewFormDefaultValues = {
title: '',
comment: '',
rating: 0,
};
export const SENDER_EMAIL = process.env.SENDER_EMAIL || 'onboarding@resend.dev';
================================================
FILE: lib/encrypt.ts
================================================
const encoder = new TextEncoder();
const key = new TextEncoder().encode(process.env.ENCRYPTION_KEY); // Retrieve key from env var
// Hash function with key-based encryption
export const hash = async (plainPassword: string): Promise => {
const passwordData = encoder.encode(plainPassword);
const cryptoKey = await crypto.subtle.importKey(
'raw',
key,
{ name: 'HMAC', hash: { name: 'SHA-256' } },
false,
['sign', 'verify']
);
const hashBuffer = await crypto.subtle.sign('HMAC', cryptoKey, passwordData);
return Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
};
// Compare function using key from env var
export const compare = async (
plainPassword: string,
encryptedPassword: string
): Promise => {
const hashedPassword = await hash(plainPassword);
return hashedPassword === encryptedPassword;
};
// // Use Web Crypto API compatible with Edge Functions
// const encoder = new TextEncoder();
// const salt = crypto.getRandomValues(new Uint8Array(16)).join('');
// // Hash function
// export const hash = async (plainPassword: string): Promise => {
// const passwordData = encoder.encode(plainPassword + salt);
// const hashBuffer = await crypto.subtle.digest('SHA-256', passwordData);
// return Array.from(new Uint8Array(hashBuffer))
// .map((b) => b.toString(16).padStart(2, '0'))
// .join('');
// };
// // Compare function
// export const compare = async (
// plainPassword: string,
// encryptedPassword: string
// ): Promise => {
// const hashedPassword = await hash(plainPassword);
// return hashedPassword === encryptedPassword;
// };
================================================
FILE: lib/paypal.ts
================================================
const base = process.env.PAYPAL_API_URL || 'https://api-m.sandbox.paypal.com';
export const paypal = {
createOrder: async function createOrder(price: number) {
const accessToken = await generateAccessToken();
const url = `${base}/v2/checkout/orders`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
intent: 'CAPTURE',
purchase_units: [
{
amount: {
currency_code: 'USD',
value: price,
},
},
],
}),
});
return handleResponse(response);
},
capturePayment: async function capturePayment(orderId: string) {
const accessToken = await generateAccessToken();
const url = `${base}/v2/checkout/orders/${orderId}/capture`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
});
return handleResponse(response);
},
};
// Generate paypal access token
async function generateAccessToken() {
const { PAYPAL_CLIENT_ID, PAYPAL_APP_SECRET } = process.env;
const auth = Buffer.from(`${PAYPAL_CLIENT_ID}:${PAYPAL_APP_SECRET}`).toString(
'base64'
);
const response = await fetch(`${base}/v1/oauth2/token`, {
method: 'POST',
body: 'grant_type=client_credentials',
headers: {
Authorization: `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
});
const jsonData = await handleResponse(response);
return jsonData.access_token;
}
async function handleResponse(response: Response) {
if (response.ok) {
return response.json();
} else {
const errorMessage = await response.text();
throw new Error(errorMessage);
}
}
export { generateAccessToken };
================================================
FILE: lib/uploadthing.ts
================================================
import {
generateUploadButton,
generateUploadDropzone,
} from '@uploadthing/react';
import type { OurFileRouter } from '@/app/api/uploadthing/core';
export const UploadButton = generateUploadButton();
export const UploadDropzone = generateUploadDropzone();
================================================
FILE: lib/utils.ts
================================================
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import qs from 'query-string';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Convert prisma object into a regular JS object
export function convertToPlainObject(value: T): T {
return JSON.parse(JSON.stringify(value));
}
// Format number with decimal places
export function formatNumberWithDecimal(num: number): string {
const [int, decimal] = num.toString().split('.');
return decimal ? `${int}.${decimal.padEnd(2, '0')}` : `${int}.00`;
}
// Format errors
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function formatError(error: any) {
if (error.name === 'ZodError') {
// Handle Zod error
const fieldErrors = Object.keys(error.errors).map(
(field) => error.errors[field].message
);
return fieldErrors.join('. ');
} else if (
error.name === 'PrismaClientKnownRequestError' &&
error.code === 'P2002'
) {
// Handle Prisma error
const field = error.meta?.target ? error.meta.target[0] : 'Field';
return `${field.charAt(0).toUpperCase() + field.slice(1)} already exists`;
} else {
// Handle other errors
return typeof error.message === 'string'
? error.message
: JSON.stringify(error.message);
}
}
// Round number to 2 decimal places
export function round2(value: number | string) {
if (typeof value === 'number') {
return Math.round((value + Number.EPSILON) * 100) / 100;
} else if (typeof value === 'string') {
return Math.round((Number(value) + Number.EPSILON) * 100) / 100;
} else {
throw new Error('Value is not a number or string');
}
}
const CURRENCY_FORMATTER = new Intl.NumberFormat('en-US', {
currency: 'USD',
style: 'currency',
minimumFractionDigits: 2,
});
// Format currency using the formatter above
export function formatCurrency(amount: number | string | null) {
if (typeof amount === 'number') {
return CURRENCY_FORMATTER.format(amount);
} else if (typeof amount === 'string') {
return CURRENCY_FORMATTER.format(Number(amount));
} else {
return 'NaN';
}
}
// Format Number
const NUMBER_FORMATTER = new Intl.NumberFormat('en-US');
export function formatNumber(number: number) {
return NUMBER_FORMATTER.format(number);
}
// Shorten UUID
export function formatId(id: string) {
return `..${id.substring(id.length - 6)}`;
}
// Format date and times
export const formatDateTime = (dateString: Date) => {
const dateTimeOptions: Intl.DateTimeFormatOptions = {
month: 'short', // abbreviated month name (e.g., 'Oct')
year: 'numeric', // abbreviated month name (e.g., 'Oct')
day: 'numeric', // numeric day of the month (e.g., '25')
hour: 'numeric', // numeric hour (e.g., '8')
minute: 'numeric', // numeric minute (e.g., '30')
hour12: true, // use 12-hour clock (true) or 24-hour clock (false)
};
const dateOptions: Intl.DateTimeFormatOptions = {
weekday: 'short', // abbreviated weekday name (e.g., 'Mon')
month: 'short', // abbreviated month name (e.g., 'Oct')
year: 'numeric', // numeric year (e.g., '2023')
day: 'numeric', // numeric day of the month (e.g., '25')
};
const timeOptions: Intl.DateTimeFormatOptions = {
hour: 'numeric', // numeric hour (e.g., '8')
minute: 'numeric', // numeric minute (e.g., '30')
hour12: true, // use 12-hour clock (true) or 24-hour clock (false)
};
const formattedDateTime: string = new Date(dateString).toLocaleString(
'en-US',
dateTimeOptions
);
const formattedDate: string = new Date(dateString).toLocaleString(
'en-US',
dateOptions
);
const formattedTime: string = new Date(dateString).toLocaleString(
'en-US',
timeOptions
);
return {
dateTime: formattedDateTime,
dateOnly: formattedDate,
timeOnly: formattedTime,
};
};
// Form the pagination links
export function formUrlQuery({
params,
key,
value,
}: {
params: string;
key: string;
value: string | null;
}) {
const query = qs.parse(params);
query[key] = value;
return qs.stringifyUrl(
{
url: window.location.pathname,
query,
},
{
skipNull: true,
}
);
}
================================================
FILE: lib/validators.ts
================================================
import { z } from 'zod';
import { formatNumberWithDecimal } from './utils';
import { PAYMENT_METHODS } from './constants';
const currency = z
.string()
.refine(
(value) => /^\d+(\.\d{2})?$/.test(formatNumberWithDecimal(Number(value))),
'Price must have exactly two decimal places'
);
// Schema for inserting products
export const insertProductSchema = z.object({
name: z.string().min(3, 'Name must be at least 3 characters'),
slug: z.string().min(3, 'Slug must be at least 3 characters'),
category: z.string().min(3, 'Category must be at least 3 characters'),
brand: z.string().min(3, 'Brand must be at least 3 characters'),
description: z.string().min(3, 'Description must be at least 3 characters'),
stock: z.coerce.number(),
images: z.array(z.string()).min(1, 'Product must have at least one image'),
isFeatured: z.boolean(),
banner: z.string().nullable(),
price: currency,
});
// Schema for updating products
export const updateProductSchema = insertProductSchema.extend({
id: z.string().min(1, 'Id is required'),
});
// Schema for signing users in
export const signInFormSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(6, 'Password must be at least 6 characters'),
});
// Schema for signing up a user
export const signUpFormSchema = z
.object({
name: z.string().min(3, 'Name must be at least 3 characters'),
email: z.string().email('Invalid email address'),
password: z.string().min(6, 'Password must be at least 6 characters'),
confirmPassword: z
.string()
.min(6, 'Confirm password must be at least 6 characters'),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
// Cart Schemas
export const cartItemSchema = z.object({
productId: z.string().min(1, 'Product is required'),
name: z.string().min(1, 'Name is required'),
slug: z.string().min(1, 'Slug is required'),
qty: z.number().int().nonnegative('Quantity must be a positive number'),
image: z.string().min(1, 'Image is required'),
price: currency,
});
export const insertCartSchema = z.object({
items: z.array(cartItemSchema),
itemsPrice: currency,
totalPrice: currency,
shippingPrice: currency,
taxPrice: currency,
sessionCartId: z.string().min(1, 'Session cart id is required'),
userId: z.string().optional().nullable(),
});
// Schema for the shipping address
export const shippingAddressSchema = z.object({
fullName: z.string().min(3, 'Name must be at least 3 characters'),
streetAddress: z.string().min(3, 'Address must be at least 3 characters'),
city: z.string().min(3, 'City must be at least 3 characters'),
postalCode: z.string().min(3, 'Postal code must be at least 3 characters'),
country: z.string().min(3, 'Country must be at least 3 characters'),
lat: z.number().optional(),
lng: z.number().optional(),
});
// Schema for payment method
export const paymentMethodSchema = z
.object({
type: z.string().min(1, 'Payment method is required'),
})
.refine((data) => PAYMENT_METHODS.includes(data.type), {
path: ['type'],
message: 'Invalid payment method',
});
// Schema for inserting order
export const insertOrderSchema = z.object({
userId: z.string().min(1, 'User is required'),
itemsPrice: currency,
shippingPrice: currency,
taxPrice: currency,
totalPrice: currency,
paymentMethod: z.string().refine((data) => PAYMENT_METHODS.includes(data), {
message: 'Invalid payment method',
}),
shippingAddress: shippingAddressSchema,
});
// Schema for inserting an order item
export const insertOrderItemSchema = z.object({
productId: z.string(),
slug: z.string(),
image: z.string(),
name: z.string(),
price: currency,
qty: z.number(),
});
// Schema for the PayPal paymentResult
export const paymentResultSchema = z.object({
id: z.string(),
status: z.string(),
email_address: z.string(),
pricePaid: z.string(),
});
// Schema for updating the user profile
export const updateProfileSchema = z.object({
name: z.string().min(3, 'Name must be at leaast 3 characters'),
email: z.string().min(3, 'Email must be at leaast 3 characters'),
});
// Schema to update users
export const updateUserSchema = updateProfileSchema.extend({
id: z.string().min(1, 'ID is required'),
role: z.string().min(1, 'Role is required'),
});
// Schema to insert reviews
export const insertReviewSchema = z.object({
title: z.string().min(3, 'Title must be at least 3 characters'),
description: z.string().min(3, 'Description must be at least 3 characters'),
productId: z.string().min(1, 'Product is required'),
userId: z.string().min(1, 'User is required'),
rating: z.coerce
.number()
.int()
.min(1, 'Rating must be at least 1')
.max(5, 'Rating must be at most 5'),
});
================================================
FILE: middleware.ts
================================================
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export const { auth: middleware } = NextAuth(authConfig);
================================================
FILE: next.config.ts
================================================
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'utfs.io',
port: '',
},
],
},
};
export default nextConfig;
================================================
FILE: package.json
================================================
{
"name": "prostore",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"postinstall": "prisma generate",
"test": "jest",
"test:watch": "jest --watch",
"email": "cp .env ./node_modules/react-email && email dev --dir email --port 3001"
},
"dependencies": {
"@auth/core": "0.37.4",
"@auth/prisma-adapter": "^2.7.4",
"@hookform/resolvers": "^3.9.1",
"@neondatabase/serverless": "^0.10.3",
"@paypal/react-paypal-js": "^8.7.0",
"@prisma/adapter-neon": "6.5.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.2",
"@react-email/components": "^0.0.31",
"@stripe/react-stripe-js": "^3.0.0",
"@stripe/stripe-js": "^5.2.0",
"@uploadthing/react": "^7.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"embla-carousel-autoplay": "^8.5.1",
"embla-carousel-react": "^8.5.1",
"lucide-react": "^0.456.0",
"next": "^15.2.2",
"next-auth": "^5.0.0-beta.25",
"next-themes": "^0.4.3",
"query-string": "^9.1.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-email": "^3.0.4",
"react-hook-form": "^7.53.2",
"recharts": "^2.14.1",
"resend": "^4.0.1",
"slugify": "^1.6.6",
"stripe": "^17.4.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"uploadthing": "^7.4.0",
"vaul": "^1.1.1",
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@prisma/client": "6.5.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.0",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/ws": "^8.5.13",
"bufferutil": "^4.0.8",
"dotenv": "^16.4.5",
"eslint": "^8",
"eslint-config-next": "15.0.3",
"jest": "^29.7.0",
"postcss": "^8",
"prisma": "6.5.0",
"tailwindcss": "^3.4.1",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "^5"
}
}
================================================
FILE: postcss.config.mjs
================================================
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
================================================
FILE: prisma/migrations/20241116125832_init/migration.sql
================================================
-- CreateTable
CREATE TABLE "Product" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"category" TEXT NOT NULL,
"images" TEXT[],
"brand" TEXT NOT NULL,
"description" TEXT NOT NULL,
"stock" INTEGER NOT NULL,
"price" DECIMAL(12,2) NOT NULL DEFAULT 0,
"rating" DECIMAL(3,2) NOT NULL DEFAULT 0,
"numReviews" INTEGER NOT NULL DEFAULT 0,
"isFeatured" BOOLEAN NOT NULL,
"banner" TEXT,
"createdAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "product_slug_idx" ON "Product"("slug");
================================================
FILE: prisma/migrations/20241118183645_add_user_based_tables/migration.sql
================================================
-- CreateTable
CREATE TABLE "User" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"name" TEXT NOT NULL DEFAULT 'NO_NAME',
"email" TEXT NOT NULL,
"emailVerified" TIMESTAMP(6),
"image" TEXT,
"password" TEXT,
"role" TEXT NOT NULL DEFAULT 'user',
"address" JSON,
"paymentMethod" TEXT,
"createdAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Account" (
"userId" UUID NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
"createdAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Account_pkey" PRIMARY KEY ("provider","providerAccountId")
);
-- CreateTable
CREATE TABLE "Session" (
"sessionToken" TEXT NOT NULL,
"userId" UUID NOT NULL,
"expires" TIMESTAMP(6) NOT NULL,
"createdAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("sessionToken")
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("identifier","token")
);
-- CreateIndex
CREATE UNIQUE INDEX "user_email_idx" ON "User"("email");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
================================================
FILE: prisma/migrations/20241121210251_add_cart/migration.sql
================================================
-- CreateTable
CREATE TABLE "Cart" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"userId" UUID,
"sessionCartId" TEXT NOT NULL,
"items" JSON[] DEFAULT ARRAY[]::JSON[],
"itemsPrice" DECIMAL(12,2) NOT NULL,
"totalPrice" DECIMAL(12,2) NOT NULL,
"shippingPrice" DECIMAL(12,2) NOT NULL,
"taxPrice" DECIMAL(12,2) NOT NULL,
"createdAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Cart_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Cart" ADD CONSTRAINT "Cart_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
================================================
FILE: prisma/migrations/20241125173259_add_order/migration.sql
================================================
-- CreateTable
CREATE TABLE "Order" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"userId" UUID NOT NULL,
"shippingAddress" JSON NOT NULL,
"paymentMethod" TEXT NOT NULL,
"paymentResult" JSON,
"itemsPrice" DECIMAL(12,2) NOT NULL,
"shippingPrice" DECIMAL(12,2) NOT NULL,
"taxPrice" DECIMAL(12,2) NOT NULL,
"totalPrice" DECIMAL(12,2) NOT NULL,
"isPaid" BOOLEAN NOT NULL DEFAULT false,
"paidAt" TIMESTAMP(6),
"isDelivered" BOOLEAN NOT NULL DEFAULT false,
"deliveredAt" TIMESTAMP(6),
"createdAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Order_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OrderItem" (
"orderId" UUID NOT NULL,
"productId" UUID NOT NULL,
"qty" INTEGER NOT NULL,
"price" DECIMAL(12,2) NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"image" TEXT NOT NULL,
CONSTRAINT "orderitems_orderId_productId_pk" PRIMARY KEY ("orderId","productId")
);
-- AddForeignKey
ALTER TABLE "Order" ADD CONSTRAINT "Order_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
================================================
FILE: prisma/migrations/20241205162619_add_featured_default/migration.sql
================================================
-- AlterTable
ALTER TABLE "Product" ALTER COLUMN "isFeatured" SET DEFAULT false;
================================================
FILE: prisma/migrations/20241209181915_add_review/migration.sql
================================================
-- CreateTable
CREATE TABLE "Review" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"userId" UUID NOT NULL,
"productId" UUID NOT NULL,
"rating" INTEGER NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"isVerifiedPurchase" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Review_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Review" ADD CONSTRAINT "Review_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Review" ADD CONSTRAINT "Review_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
================================================
FILE: prisma/migrations/migration_lock.toml
================================================
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
================================================
FILE: prisma/schema.prisma
================================================
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Product {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String
slug String @unique(map: "product_slug_idx")
category String
images String[]
brand String
description String
stock Int
price Decimal @default(0) @db.Decimal(12, 2)
rating Decimal @default(0) @db.Decimal(3, 2)
numReviews Int @default(0)
isFeatured Boolean @default(false)
banner String?
createdAt DateTime @default(now()) @db.Timestamp(6)
OrderItem OrderItem[]
Review Review[]
}
model User {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String @default("NO_NAME")
email String @unique(map: "user_email_idx")
emailVerified DateTime? @db.Timestamp(6)
image String?
password String?
role String @default("user")
address Json? @db.Json
paymentMethod String?
createdAt DateTime @default(now()) @db.Timestamp(6)
updatedAt DateTime @updatedAt
account Account[]
session Session[]
Cart Cart[]
Order Order[]
Review Review[]
}
model Account {
userId String @db.Uuid
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
createdAt DateTime @default(now()) @db.Timestamp(6)
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([provider, providerAccountId])
}
model Session {
sessionToken String @id
userId String @db.Uuid
expires DateTime @db.Timestamp(6)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @db.Timestamp(6)
updatedAt DateTime @updatedAt
}
model VerificationToken {
identifier String
token String
expires DateTime
@@id([identifier, token])
}
model Cart {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
userId String? @db.Uuid
sessionCartId String
items Json[] @default([]) @db.Json
itemsPrice Decimal @db.Decimal(12, 2)
totalPrice Decimal @db.Decimal(12, 2)
shippingPrice Decimal @db.Decimal(12, 2)
taxPrice Decimal @db.Decimal(12, 2)
createdAt DateTime @default(now()) @db.Timestamp(6)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Order {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
userId String @db.Uuid
shippingAddress Json @db.Json
paymentMethod String
paymentResult Json? @db.Json
itemsPrice Decimal @db.Decimal(12, 2)
shippingPrice Decimal @db.Decimal(12, 2)
taxPrice Decimal @db.Decimal(12, 2)
totalPrice Decimal @db.Decimal(12, 2)
isPaid Boolean @default(false)
paidAt DateTime? @db.Timestamp(6)
isDelivered Boolean @default(false)
deliveredAt DateTime? @db.Timestamp(6)
createdAt DateTime @default(now()) @db.Timestamp(6)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
orderitems OrderItem[]
}
model OrderItem {
orderId String @db.Uuid
productId String @db.Uuid
qty Int
price Decimal @db.Decimal(12, 2)
name String
slug String
image String
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
@@id([orderId, productId], map: "orderitems_orderId_productId_pk")
}
model Review {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
userId String @db.Uuid
productId String @db.Uuid
rating Int
title String
description String
isVerifiedPurchase Boolean @default(true)
createdAt DateTime @default(now()) @db.Timestamp(6)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
================================================
FILE: tailwind.config.ts
================================================
import type { Config } from "tailwindcss";
export default {
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;
================================================
FILE: tests/paypal.test.ts
================================================
import { generateAccessToken, paypal } from '../lib/paypal';
// Test to generate access token from paypal
test('generates token from paypal', async () => {
const tokenResponse = await generateAccessToken();
console.log(tokenResponse);
expect(typeof tokenResponse).toBe('string');
expect(tokenResponse.length).toBeGreaterThan(0);
});
// Test to create a paypal order
test('creates a paypal order', async () => {
const token = await generateAccessToken();
const price = 10.0;
const orderResponse = await paypal.createOrder(price);
console.log(orderResponse);
expect(orderResponse).toHaveProperty('id');
expect(orderResponse).toHaveProperty('status');
expect(orderResponse.status).toBe('CREATED');
});
// Test to capture payment with mock order
test('simulate capturing a payment from an order', async () => {
const orderId = '100';
const mockCapturePayment = jest
.spyOn(paypal, 'capturePayment')
.mockResolvedValue({
status: 'COMPLETED',
});
const captureResponse = await paypal.capturePayment(orderId);
expect(captureResponse).toHaveProperty('status', 'COMPLETED');
mockCapturePayment.mockRestore();
});
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: types/index.ts
================================================
import { z } from 'zod';
import {
insertProductSchema,
insertCartSchema,
cartItemSchema,
shippingAddressSchema,
insertOrderItemSchema,
insertOrderSchema,
paymentResultSchema,
insertReviewSchema,
} from '@/lib/validators';
export type Product = z.infer & {
id: string;
rating: string;
numReviews: number;
createdAt: Date;
};
export type Cart = z.infer;
export type CartItem = z.infer;
export type ShippingAddress = z.infer;
export type OrderItem = z.infer;
export type Order = z.infer & {
id: string;
createdAt: Date;
isPaid: boolean;
paidAt: Date | null;
isDelivered: boolean;
deliveredAt: Date | null;
orderitems: OrderItem[];
user: { name: string; email: string };
paymentResult: PaymentResult;
};
export type PaymentResult = z.infer;
export type Review = z.infer & {
id: string;
createdAt: Date;
user?: { name: string };
};
================================================
FILE: types/next-auth.d.ts
================================================
import { DefaultSession } from 'next-auth';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import NextAuth from 'next-auth';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { JWT } from 'next-auth/jwt';
declare module 'next-auth/jwt' {
/** Returned by the `jwt` callback and `getToken`, when using JWT sessions */
interface JWT {
sub: string;
role: string;
name: string;
}
}
declare module 'next-auth' {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
interface Session {
user: {
role: string;
} & DefaultSession['user'];
}
interface User {
role: string;
}
}