Repository: felipemotarocha/fullstackweek-foods Branch: main Commit: 2d12da30da96 Files: 76 Total size: 124.0 KB Directory structure: gitextract_3npftlea/ ├── .eslintrc.json ├── .gitignore ├── .husky/ │ ├── commit-msg │ └── pre-commit ├── .lintstagedrc.json ├── .prettierrc ├── README.md ├── app/ │ ├── _actions/ │ │ ├── order.ts │ │ └── restaurant.ts │ ├── _components/ │ │ ├── cart-item.tsx │ │ ├── cart.tsx │ │ ├── category-item.tsx │ │ ├── category-list.tsx │ │ ├── delivery-info.tsx │ │ ├── discount-badge.tsx │ │ ├── header.tsx │ │ ├── product-item.tsx │ │ ├── product-list.tsx │ │ ├── promo-banner.tsx │ │ ├── restaurant-item.tsx │ │ ├── restaurant-list.tsx │ │ ├── search.tsx │ │ └── ui/ │ │ ├── alert-dialog.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── input.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ └── sonner.tsx │ ├── _context/ │ │ └── cart.tsx │ ├── _helpers/ │ │ ├── price.ts │ │ └── restaurant.ts │ ├── _hooks/ │ │ └── use-toggle-favorite-restaurant.ts │ ├── _lib/ │ │ ├── auth.ts │ │ ├── prisma.ts │ │ └── utils.ts │ ├── _providers/ │ │ └── auth.tsx │ ├── api/ │ │ └── auth/ │ │ └── [...nextauth]/ │ │ └── route.ts │ ├── categories/ │ │ └── [id]/ │ │ └── products/ │ │ └── page.tsx │ ├── globals.css │ ├── layout.tsx │ ├── my-favorite-restaurants/ │ │ └── page.tsx │ ├── my-orders/ │ │ ├── _components/ │ │ │ └── order-item.tsx │ │ └── page.tsx │ ├── page.tsx │ ├── products/ │ │ ├── [id]/ │ │ │ ├── _components/ │ │ │ │ ├── product-details.tsx │ │ │ │ └── product-image.tsx │ │ │ └── page.tsx │ │ └── recommended/ │ │ └── page.tsx │ └── restaurants/ │ ├── [id]/ │ │ ├── _components/ │ │ │ ├── cart-banner.tsx │ │ │ └── restaurant-image.tsx │ │ └── page.tsx │ ├── _actions/ │ │ └── search.ts │ ├── _components/ │ │ └── restaurants.tsx │ ├── page.tsx │ └── recommended/ │ └── page.tsx ├── components.json ├── docker-compose.yml ├── next-auth.d.ts ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── prisma/ │ ├── migrations/ │ │ ├── 20240415210903_init_database/ │ │ │ └── migration.sql │ │ ├── 20240501004806_add_created_at/ │ │ │ └── migration.sql │ │ ├── 20240502232158_add_auth_tables/ │ │ │ └── migration.sql │ │ ├── 20240503000412_add_order_table/ │ │ │ └── migration.sql │ │ ├── 20240503012547_add_order_product_table/ │ │ │ └── migration.sql │ │ ├── 20240503012822_add_order_product_table/ │ │ │ └── migration.sql │ │ ├── 20240503233901_add_user_restaurant_favorites_table/ │ │ │ └── migration.sql │ │ ├── 20240503235713_add_compound_key_to_user_favorite_restaurant/ │ │ │ └── migration.sql │ │ └── migration_lock.toml │ ├── schema.prisma │ └── seed.ts ├── tailwind.config.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": "next/core-web-vitals", "rules": { "no-unused-vars": "error" } } ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/install-state.gz # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local .env # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts # docker .postgres-data ================================================ FILE: .husky/commit-msg ================================================ #!/bin/sh . "$(dirname "$0")/_/husky.sh" .git/hooks/commit-msg $1 ================================================ FILE: .husky/pre-commit ================================================ npx lint-staged ================================================ FILE: .lintstagedrc.json ================================================ { "*.ts?(x)": ["eslint --fix", "prettier --write"] } ================================================ FILE: .prettierrc ================================================ { "plugins": ["prettier-plugin-tailwindcss"] } ================================================ FILE: README.md ================================================ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). ## Getting Started First, run the development server: ```bash npm run dev # or yarn dev # or pnpm dev # or bun dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. ## Learn More To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. ================================================ FILE: app/_actions/order.ts ================================================ "use server"; import { Prisma } from "@prisma/client"; import { db } from "../_lib/prisma"; import { revalidatePath } from "next/cache"; export const createOrder = async (data: Prisma.OrderCreateInput) => { await db.order.create({ data }); revalidatePath("/my-orders"); }; ================================================ FILE: app/_actions/restaurant.ts ================================================ "use server"; import { revalidatePath } from "next/cache"; import { db } from "../_lib/prisma"; export const toggleFavoriteRestaurant = async ( userId: string, restaurantId: string, ) => { const isFavorite = await db.userFavoriteRestaurant.findFirst({ where: { userId, restaurantId, }, }); if (isFavorite) { await db.userFavoriteRestaurant.delete({ where: { userId_restaurantId: { userId, restaurantId, }, }, }); revalidatePath("/"); return; } await db.userFavoriteRestaurant.create({ data: { userId, restaurantId, }, }); revalidatePath("/"); }; ================================================ FILE: app/_components/cart-item.tsx ================================================ import Image from "next/image"; import { CartContext, CartProduct } from "../_context/cart"; import { calculateProductTotalPrice, formatCurrency } from "../_helpers/price"; import { Button } from "./ui/button"; import { ChevronLeftIcon, ChevronRightIcon, TrashIcon } from "lucide-react"; import { memo, useContext } from "react"; interface CartItemProps { cartProduct: CartProduct; } const CartItem = ({ cartProduct }: CartItemProps) => { const { decreaseProductQuantity, increaseProductQuantity, removeProductFromCart, } = useContext(CartContext); const handleDecreaseQuantityClick = () => decreaseProductQuantity(cartProduct.id); const handleIncreaseQuantityClick = () => increaseProductQuantity(cartProduct.id); const handleRemoveClick = () => removeProductFromCart(cartProduct.id); return (
{/* IMAGEM E INFO */}
{cartProduct.name}

{cartProduct.name}

{formatCurrency( calculateProductTotalPrice(cartProduct) * cartProduct.quantity, )}

{cartProduct.discountPercentage > 0 && ( {formatCurrency( Number(cartProduct.price) * cartProduct.quantity, )} )}
{/* QUANTIDADE */}

{cartProduct.quantity}

{/* BOTÃO DE DELETAR */}
); }; export default memo(CartItem, (prev, next) => { return prev.cartProduct.quantity === next.cartProduct.quantity; }); ================================================ FILE: app/_components/cart.tsx ================================================ import { useContext, useState } from "react"; import { CartContext } from "../_context/cart"; import CartItem from "./cart-item"; import { Card, CardContent } from "./ui/card"; import { formatCurrency } from "../_helpers/price"; import { Separator } from "./ui/separator"; import { Button } from "./ui/button"; import { createOrder } from "../_actions/order"; import { OrderStatus } from "@prisma/client"; import { useSession } from "next-auth/react"; import { Loader2 } from "lucide-react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "./ui/alert-dialog"; import { toast } from "sonner"; import { useRouter } from "next/navigation"; interface CartProps { // eslint-disable-next-line no-unused-vars setIsOpen: (isOpen: boolean) => void; } const Cart = ({ setIsOpen }: CartProps) => { const router = useRouter(); const [isSubmitLoading, setIsSubmitLoading] = useState(false); const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); const { data } = useSession(); const { products, subtotalPrice, totalPrice, totalDiscounts, clearCart } = useContext(CartContext); const handleFinishOrderClick = async () => { if (!data?.user) return; const restaurant = products[0].restaurant; try { setIsSubmitLoading(true); await createOrder({ subtotalPrice, totalDiscounts, totalPrice, deliveryFee: restaurant.deliveryFee, deliveryTimeMinutes: restaurant.deliveryTimeMinutes, restaurant: { connect: { id: restaurant.id }, }, status: OrderStatus.CONFIRMED, user: { connect: { id: data.user.id }, }, products: { createMany: { data: products.map((product) => ({ productId: product.id, quantity: product.quantity, })), }, }, }); clearCart(); setIsOpen(false); toast("Pedido finalizado com sucesso!", { description: "Você pode acompanhá-lo na tela dos seus pedidos.", action: { label: "Meus Pedidos", onClick: () => router.push("/my-orders"), }, }); } catch (error) { console.error(error); } finally { setIsSubmitLoading(false); } }; return ( <>
{products.length > 0 ? ( <>
{products.map((product) => ( ))}
{/* TOTAIS */}
Subtotal {formatCurrency(subtotalPrice)}
Descontos - {formatCurrency(totalDiscounts)}
Entrega {Number(products?.[0].restaurant.deliveryFee) === 0 ? ( Grátis ) : ( formatCurrency( Number(products?.[0].restaurant.deliveryFee), ) )}
Total {formatCurrency(totalPrice)}
{/* FINALIZAR PEDIDO */} ) : (

Sua sacola está vazia.

)}
Deseja finalizar seu pedido? Ao finalizar seu pedido, você concorda com os termos e condições da nossa plataforma. Cancelar {isSubmitLoading && ( )} Finalizar ); }; export default Cart; ================================================ FILE: app/_components/category-item.tsx ================================================ import { Category } from "@prisma/client"; import Image from "next/image"; import Link from "next/link"; interface CategoryItemProps { category: Category; } const CategoryItem = ({ category }: CategoryItemProps) => { return ( {category.name} {category.name} ); }; export default CategoryItem; ================================================ FILE: app/_components/category-list.tsx ================================================ import { db } from "../_lib/prisma"; import CategoryItem from "./category-item"; const CategoryList = async () => { const categories = await db.category.findMany({}); return (
{categories.map((category) => ( ))}
); }; export default CategoryList; ================================================ FILE: app/_components/delivery-info.tsx ================================================ import { BikeIcon, TimerIcon } from "lucide-react"; import { Card } from "./ui/card"; import { formatCurrency } from "../_helpers/price"; import { Restaurant } from "@prisma/client"; interface DeliveryInfoProps { restaurant: Pick; } const DeliveryInfo = ({ restaurant }: DeliveryInfoProps) => { return ( <> {/* CUSTO */}
Entrega
{Number(restaurant.deliveryFee) > 0 ? (

{formatCurrency(Number(restaurant.deliveryFee))}

) : (

Grátis

)}
{/* TEMPO */}
Entrega

{restaurant.deliveryTimeMinutes} min

); }; export default DeliveryInfo; ================================================ FILE: app/_components/discount-badge.tsx ================================================ import { Product } from "@prisma/client"; import { ArrowDownIcon } from "lucide-react"; interface DiscountBadgeProps { product: Pick; } const DiscountBadge = ({ product }: DiscountBadgeProps) => { return (
{product.discountPercentage}%
); }; export default DiscountBadge; ================================================ FILE: app/_components/header.tsx ================================================ "use client"; import Image from "next/image"; import { Button } from "./ui/button"; import { HeartIcon, HomeIcon, LogInIcon, LogOutIcon, MenuIcon, ScrollTextIcon, } from "lucide-react"; import Link from "next/link"; import { signIn, signOut, useSession } from "next-auth/react"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, } from "./ui/sheet"; import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; import { Separator } from "./ui/separator"; const Header = () => { const { data } = useSession(); const handleSignOutClick = () => signOut(); const handleSignInClick = () => signIn(); return (
FSW Foods
Menu {data?.user ? ( <>
{data?.user?.name?.split(" ")[0][0]} {data?.user?.name?.split(" ")[1][0]}

{data?.user?.name}

{data?.user?.email}
) : ( <>

Olá. Faça seu login!

)}
{data?.user && ( <> )}
{data?.user && ( )}
); }; export default Header; ================================================ FILE: app/_components/product-item.tsx ================================================ "use client"; import { Prisma } from "@prisma/client"; import Image from "next/image"; import { calculateProductTotalPrice, formatCurrency } from "../_helpers/price"; import { ArrowDownIcon } from "lucide-react"; import Link from "next/link"; import { cn } from "../_lib/utils"; interface ProductItemProps { product: Prisma.ProductGetPayload<{ include: { restaurant: { select: { name: true; }; }; }; }>; className?: string; } const ProductItem = ({ product, className }: ProductItemProps) => { return (
{product.name} {product.discountPercentage && (
{product.discountPercentage}%
)}

{product.name}

{formatCurrency(calculateProductTotalPrice(product))}

{product.discountPercentage > 0 && ( {formatCurrency(Number(product.price))} )}
{product.restaurant.name}
); }; export default ProductItem; ================================================ FILE: app/_components/product-list.tsx ================================================ import { Prisma } from "@prisma/client"; import ProductItem from "./product-item"; interface ProductListProps { products: Prisma.ProductGetPayload<{ include: { restaurant: { select: { name: true; }; }; }; }>[]; } const ProductList = ({ products }: ProductListProps) => { return (
{products.map((product) => ( ))}
); }; export default ProductList; ================================================ FILE: app/_components/promo-banner.tsx ================================================ import Image, { ImageProps } from "next/image"; const PromoBanner = (props: ImageProps) => { return ( ); }; export default PromoBanner; ================================================ FILE: app/_components/restaurant-item.tsx ================================================ "use client"; import { Restaurant, UserFavoriteRestaurant } from "@prisma/client"; import { BikeIcon, HeartIcon, StarIcon, TimerIcon } from "lucide-react"; import Image from "next/image"; import { formatCurrency } from "../_helpers/price"; import { Button } from "./ui/button"; import Link from "next/link"; import { cn } from "../_lib/utils"; import { toggleFavoriteRestaurant } from "../_actions/restaurant"; import { toast } from "sonner"; import { useSession } from "next-auth/react"; interface RestaurantItemProps { restaurant: Restaurant; className?: string; userFavoriteRestaurants: UserFavoriteRestaurant[]; } const RestaurantItem = ({ restaurant, className, userFavoriteRestaurants, }: RestaurantItemProps) => { const { data } = useSession(); const isFavorite = userFavoriteRestaurants.some( (fav) => fav.restaurantId === restaurant.id, ); const handleFavoriteClick = async () => { if (!data?.user.id) return; try { await toggleFavoriteRestaurant(data?.user.id, restaurant.id); toast.success( isFavorite ? "Restaurante removido dos favoritos." : "Restaurante favoritado.", ); } catch (error) { toast.error("Erro ao favoritar restaurante."); } }; return (
{/* IMAGEM */}
{restaurant.name}
5.0
{data?.user.id && ( )}
{/* TEXTO */}

{restaurant.name}

{/* INFORMAÇÕES DA ENTREGA */}
{/* CUSTO DE ENTREGA */}
{Number(restaurant.deliveryFee) === 0 ? "Entrega grátis" : formatCurrency(Number(restaurant.deliveryFee))}
{/* TEMPO DE ENTREGA */}
{restaurant.deliveryTimeMinutes} min
); }; export default RestaurantItem; ================================================ FILE: app/_components/restaurant-list.tsx ================================================ import { getServerSession } from "next-auth"; import { db } from "../_lib/prisma"; import RestaurantItem from "./restaurant-item"; import { authOptions } from "../_lib/auth"; const RestaurantList = async () => { const session = await getServerSession(authOptions); // TODO: pegar restaurantes com maior número de pedidos const restaurants = await db.restaurant.findMany({ take: 10 }); const userFavoriteRestaurants = await db.userFavoriteRestaurant.findMany({ where: { userId: session?.user?.id }, }); return (
{restaurants.map((restaurant) => ( ))}
); }; export default RestaurantList; ================================================ FILE: app/_components/search.tsx ================================================ "use client"; import { SearchIcon } from "lucide-react"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { FormEventHandler, useState } from "react"; import { useRouter } from "next/navigation"; const Search = () => { const router = useRouter(); const [search, setSearch] = useState(""); const handleChange = (e: React.ChangeEvent) => { setSearch(e.target.value); }; const handleSearchSubmit: FormEventHandler = (e) => { e.preventDefault(); if (!search) { return; } router.push(`/restaurants?search=${search}`); }; return (
); }; export default Search; ================================================ FILE: app/_components/ui/alert-dialog.tsx ================================================ "use client"; import * as React from "react"; import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; import { cn } from "@/app/_lib/utils"; import { buttonVariants } from "@/app/_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: app/_components/ui/avatar.tsx ================================================ "use client"; import * as React from "react"; import * as AvatarPrimitive from "@radix-ui/react-avatar"; import { cn } from "@/app/_lib/utils"; const Avatar = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; export { Avatar, AvatarImage, AvatarFallback }; ================================================ FILE: app/_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 "@/app/_lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-semibold 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", { 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: app/_components/ui/card.tsx ================================================ import * as React from "react"; import { cn } from "@/app/_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< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)); CardTitle.displayName = "CardTitle"; const CardDescription = React.forwardRef< HTMLParagraphElement, 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: app/_components/ui/input.tsx ================================================ import * as React from "react"; import { cn } from "@/app/_lib/utils"; export interface InputProps extends React.InputHTMLAttributes {} const Input = React.forwardRef( ({ className, type, ...props }, ref) => { return ( ); }, ); Input.displayName = "Input"; export { Input }; ================================================ FILE: app/_components/ui/separator.tsx ================================================ "use client"; import * as React from "react"; import * as SeparatorPrimitive from "@radix-ui/react-separator"; import { cn } from "@/app/_lib/utils"; const Separator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >( ( { className, orientation = "horizontal", decorative = true, ...props }, ref, ) => ( ), ); Separator.displayName = SeparatorPrimitive.Root.displayName; export { Separator }; ================================================ FILE: app/_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 "@/app/_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-white 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: app/_components/ui/sonner.tsx ================================================ "use client"; import { useTheme } from "next-themes"; import { Toaster as Sonner } from "sonner"; type ToasterProps = React.ComponentProps; const Toaster = ({ ...props }: ToasterProps) => { const { theme = "system" } = useTheme(); return ( ); }; export { Toaster }; ================================================ FILE: app/_context/cart.tsx ================================================ /* eslint-disable no-unused-vars */ "use client"; import { Prisma } from "@prisma/client"; import { ReactNode, createContext, useState } from "react"; import { calculateProductTotalPrice } from "../_helpers/price"; export interface CartProduct extends Prisma.ProductGetPayload<{ include: { restaurant: { select: { id: true; deliveryFee: true; deliveryTimeMinutes: true; }; }; }; }> { quantity: number; } interface ICartContext { products: CartProduct[]; subtotalPrice: number; totalPrice: number; totalDiscounts: number; totalQuantity: number; addProductToCart: ({ product, emptyCart, }: { product: CartProduct; emptyCart?: boolean; }) => void; decreaseProductQuantity: (productId: string) => void; increaseProductQuantity: (productId: string) => void; removeProductFromCart: (productId: string) => void; clearCart: () => void; } export const CartContext = createContext({ products: [], subtotalPrice: 0, totalPrice: 0, totalDiscounts: 0, totalQuantity: 0, addProductToCart: () => {}, decreaseProductQuantity: () => {}, increaseProductQuantity: () => {}, removeProductFromCart: () => {}, clearCart: () => {}, }); export const CartProvider = ({ children }: { children: ReactNode }) => { const [products, setProducts] = useState([]); const subtotalPrice = products.reduce((acc, product) => { return acc + Number(product.price) * product.quantity; }, 0); const totalPrice = products.reduce((acc, product) => { return acc + calculateProductTotalPrice(product) * product.quantity; }, 0) + Number(products?.[0]?.restaurant?.deliveryFee); const totalQuantity = products.reduce((acc, product) => { return acc + product.quantity; }, 0); const totalDiscounts = subtotalPrice - totalPrice + Number(products?.[0]?.restaurant?.deliveryFee); const clearCart = () => { return setProducts([]); }; const decreaseProductQuantity: ICartContext["decreaseProductQuantity"] = ( productId: string, ) => { return setProducts((prev) => prev.map((cartProduct) => { if (cartProduct.id === productId) { if (cartProduct.quantity === 1) { return cartProduct; } return { ...cartProduct, quantity: cartProduct.quantity - 1, }; } return cartProduct; }), ); }; const increaseProductQuantity: ICartContext["increaseProductQuantity"] = ( productId: string, ) => { return setProducts((prev) => prev.map((cartProduct) => { if (cartProduct.id === productId) { return { ...cartProduct, quantity: cartProduct.quantity + 1, }; } return cartProduct; }), ); }; const removeProductFromCart: ICartContext["removeProductFromCart"] = ( productId: string, ) => { return setProducts((prev) => prev.filter((product) => product.id !== productId), ); }; const addProductToCart: ICartContext["addProductToCart"] = ({ product, emptyCart, }) => { if (emptyCart) { setProducts([]); } // VERIFICAR SE O PRODUTO JÁ ESTÁ NO CARRINHO const isProductAlreadyOnCart = products.some( (cartProduct) => cartProduct.id === product.id, ); // SE ELE ESTIVER, AUMENTAR A SUA QUANTIDADE if (isProductAlreadyOnCart) { return setProducts((prev) => prev.map((cartProduct) => { if (cartProduct.id === product.id) { return { ...cartProduct, quantity: cartProduct.quantity + product.quantity, }; } return cartProduct; }), ); } // SE NÃO, ADICIONÁ-LO COM A QUANTIDADE RECEBIDA setProducts((prev) => [...prev, product]); }; return ( {children} ); }; ================================================ FILE: app/_helpers/price.ts ================================================ import { Product } from "@prisma/client"; export const calculateProductTotalPrice = (product: Product): number => { if (product.discountPercentage === 0) { return Number(product.price); } const discount = Number(product.price) * (product.discountPercentage / 100); return Number(product.price) - discount; }; export const formatCurrency = (value: number): string => { return `R$${Intl.NumberFormat("pt-BR", { currency: "BRL", minimumFractionDigits: 2, }).format(value)}`; }; ================================================ FILE: app/_helpers/restaurant.ts ================================================ import { UserFavoriteRestaurant } from "@prisma/client"; export const isRestaurantFavorited = ( restaurantId: string, userFavoriteRestaurants: UserFavoriteRestaurant[], ) => userFavoriteRestaurants?.some((fav) => fav.restaurantId === restaurantId); ================================================ FILE: app/_hooks/use-toggle-favorite-restaurant.ts ================================================ import { toast } from "sonner"; import { toggleFavoriteRestaurant } from "../_actions/restaurant"; import { UserFavoriteRestaurant } from "@prisma/client"; import { useRouter } from "next/navigation"; interface UseToggleFavoriteRestaurantProps { userId?: string; userFavoriteRestaurants?: UserFavoriteRestaurant[]; restaurantId: string; restaurantIsFavorited?: boolean; } const useToggleFavoriteRestaurant = ({ userId, restaurantId, restaurantIsFavorited, }: UseToggleFavoriteRestaurantProps) => { const router = useRouter(); const handleFavoriteClick = async () => { if (!userId) return; try { await toggleFavoriteRestaurant(userId, restaurantId); toast( restaurantIsFavorited ? "Restaurante removido dos favoritos." : "Restaurante favoritado.", { action: { label: "Ver Favoritos", onClick: () => router.push("/my-favorite-restaurants"), }, }, ); } catch (error) { toast.error("Erro ao favoritar restaurante."); } }; return { handleFavoriteClick }; }; export default useToggleFavoriteRestaurant; ================================================ FILE: app/_lib/auth.ts ================================================ import { PrismaAdapter } from "@auth/prisma-adapter"; import { AuthOptions } from "next-auth"; import { db } from "./prisma"; import GoogleProvider from "next-auth/providers/google"; import { Adapter } from "next-auth/adapters"; export const authOptions: AuthOptions = { adapter: PrismaAdapter(db) as Adapter, providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, }), ], callbacks: { async session({ session, user }) { session.user = { ...session.user, id: user.id }; return session; }, }, secret: process.env.NEXTAUTH_SECRET, }; ================================================ FILE: app/_lib/prisma.ts ================================================ /* eslint-disable no-unused-vars */ import { PrismaClient } from "@prisma/client"; declare global { var cachedPrisma: PrismaClient; } let prisma: PrismaClient; if (process.env.NODE_ENV === "production") { prisma = new PrismaClient(); } else { if (!global.cachedPrisma) { global.cachedPrisma = new PrismaClient(); } prisma = global.cachedPrisma; } export const db = prisma; ================================================ FILE: app/_lib/utils.ts ================================================ import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } ================================================ FILE: app/_providers/auth.tsx ================================================ "use client"; import { SessionProvider } from "next-auth/react"; import { ReactNode } from "react"; const AuthProvider = ({ children }: { children: ReactNode }) => { return {children}; }; export default AuthProvider; ================================================ FILE: app/api/auth/[...nextauth]/route.ts ================================================ import NextAuth from "next-auth"; import { authOptions } from "@/app/_lib/auth"; const handler = NextAuth(authOptions); export { handler as GET, handler as POST }; ================================================ FILE: app/categories/[id]/products/page.tsx ================================================ import Header from "@/app/_components/header"; import ProductItem from "@/app/_components/product-item"; import { db } from "@/app/_lib/prisma"; import { notFound } from "next/navigation"; interface CategoriesPageProps { params: { id: string; }; } const CategoriesPage = async ({ params: { id } }: CategoriesPageProps) => { const category = await db.category.findUnique({ where: { id, }, include: { products: { include: { restaurant: { select: { name: true, }, }, }, }, }, }); if (!category) { return notFound(); } return ( <>

{category.name}

{category.products.map((product) => ( ))}
); }; export default CategoriesPage; ================================================ FILE: app/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; :root { --background: 0 0% 96%; --foreground: 0 0% 20%; --card: 356 50% 100%; --card-foreground: 356 5% 15%; --popover: 356 100% 100%; --popover-foreground: 356 100% 10%; --primary: 356 83% 51.6%; --primary-foreground: 0 0% 100%; --secondary: 356 30% 90%; --secondary-foreground: 0 0% 0%; --muted: 318 30% 95%; --muted-foreground: 225 8% 53%; --accent: 318 30% 90%; --accent-foreground: 356 5% 15%; --destructive: 0 100% 50%; --destructive-foreground: 356 5% 100%; --border: 356 30% 82%; --input: 356 30% 50%; --ring: 356 83% 51.6%; --radius: 0.75rem; } .dark { --background: 356 50% 10%; --foreground: 356 5% 100%; --card: 356 50% 10%; --card-foreground: 356 5% 100%; --popover: 356 50% 5%; --popover-foreground: 356 5% 100%; --primary: 356 83% 51.6%; --primary-foreground: 0 0% 100%; --secondary: 356 30% 20%; --secondary-foreground: 0 0% 100%; --muted: 318 30% 25%; --muted-foreground: 225 8% 53%; --accent: 318 30% 25%; --accent-foreground: 356 5% 95%; --destructive: 0 100% 50%; --destructive-foreground: 356 5% 100%; --border: 356 30% 50%; --input: 356 30% 50%; --ring: 356 83% 51.6%; --radius: 0.75rem; } @layer base { * { @apply box-border; } body { @apply text-foreground antialiased; } } ================================================ FILE: app/layout.tsx ================================================ import type { Metadata } from "next"; import { Poppins } from "next/font/google"; import "./globals.css"; import { CartProvider } from "./_context/cart"; import AuthProvider from "./_providers/auth"; import { Toaster } from "@/app/_components/ui/sonner"; const poppins = Poppins({ subsets: ["latin"], weight: ["400", "500", "600", "700", "800"], }); export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( {children} ); } ================================================ FILE: app/my-favorite-restaurants/page.tsx ================================================ import { getServerSession } from "next-auth"; import { db } from "../_lib/prisma"; import { authOptions } from "../_lib/auth"; import { notFound } from "next/navigation"; import Header from "../_components/header"; import RestaurantItem from "../_components/restaurant-item"; const MyFavoriteRestaurants = async () => { const session = await getServerSession(authOptions); if (!session) { return notFound(); } const userFavoriteRestaurants = await db.userFavoriteRestaurant.findMany({ where: { userId: session.user.id, }, include: { restaurant: true, }, }); return ( <>

Restaurantes Favoritos

{userFavoriteRestaurants.length > 0 ? ( userFavoriteRestaurants.map(({ restaurant }) => ( )) ) : (

Você ainda não marcou nenhum restaurante como favorito.

)}
); }; export default MyFavoriteRestaurants; ================================================ FILE: app/my-orders/_components/order-item.tsx ================================================ "use client"; import { Avatar, AvatarImage } from "@/app/_components/ui/avatar"; import { Button } from "@/app/_components/ui/button"; import { Card, CardContent } from "@/app/_components/ui/card"; import { Separator } from "@/app/_components/ui/separator"; import { CartContext } from "@/app/_context/cart"; import { formatCurrency } from "@/app/_helpers/price"; import { OrderStatus, Prisma } from "@prisma/client"; import { ChevronRightIcon } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useContext } from "react"; interface OrderItemProps { order: Prisma.OrderGetPayload<{ include: { restaurant: true; products: { include: { product: true; }; }; }; }>; } const getOrderStatusLabel = (status: OrderStatus) => { switch (status) { case "CANCELED": return "Cancelado"; case "COMPLETED": return "Finalizado"; case "CONFIRMED": return "Confirmado"; case "DELIVERING": return "Em Transporte"; case "PREPARING": return "Preparando"; } }; const OrderItem = ({ order }: OrderItemProps) => { const { addProductToCart } = useContext(CartContext); const router = useRouter(); const handleRedoOrderClick = () => { for (const orderProduct of order.products) { addProductToCart({ product: { ...orderProduct.product, restaurant: order.restaurant, quantity: orderProduct.quantity, }, }); } router.push(`/restaurants/${order.restaurantId}`); }; return (
{getOrderStatusLabel(order.status)}
{order.restaurant.name}
{order.products.map((product) => (
{product.quantity}
{product.product.name}
))}

{formatCurrency(Number(order.totalPrice))}

); }; export default OrderItem; ================================================ FILE: app/my-orders/page.tsx ================================================ import { getServerSession } from "next-auth"; import { db } from "../_lib/prisma"; import { authOptions } from "../_lib/auth"; import { redirect } from "next/navigation"; import Header from "../_components/header"; import OrderItem from "./_components/order-item"; const MyOrdersPage = async () => { const session = await getServerSession(authOptions); if (!session?.user) { return redirect("/"); } const orders = await db.order.findMany({ where: { userId: session.user.id, }, include: { restaurant: true, products: { include: { product: true, }, }, }, }); return ( <>

Meus Pedidos

{orders.map((order) => ( ))}
); }; export default MyOrdersPage; ================================================ FILE: app/page.tsx ================================================ import CategoryList from "./_components/category-list"; import Header from "./_components/header"; import Search from "./_components/search"; import ProductList from "./_components/product-list"; import { Button } from "./_components/ui/button"; import { ChevronRightIcon } from "lucide-react"; import { db } from "./_lib/prisma"; import PromoBanner from "./_components/promo-banner"; import RestaurantList from "./_components/restaurant-list"; import Link from "next/link"; const fetch = async () => { const getProducts = db.product.findMany({ where: { discountPercentage: { gt: 0, }, }, take: 10, include: { restaurant: { select: { name: true, }, }, }, }); const getBurguersCategory = db.category.findFirst({ where: { name: "Hambúrgueres", }, }); const getPizzasCategory = db.category.findFirst({ where: { name: "Pizzas", }, }); const [products, burguersCategory, pizzasCategory] = await Promise.all([ getProducts, getBurguersCategory, getPizzasCategory, ]); return { products, burguersCategory, pizzasCategory }; }; const Home = async () => { const { products, burguersCategory, pizzasCategory } = await fetch(); return ( <>

Pedidos Recomendados

Restaurantes Recomendados

); }; export default Home; ================================================ FILE: app/products/[id]/_components/product-details.tsx ================================================ "use client"; import Cart from "@/app/_components/cart"; import DeliveryInfo from "@/app/_components/delivery-info"; import DiscountBadge from "@/app/_components/discount-badge"; import ProductList from "@/app/_components/product-list"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/app/_components/ui/alert-dialog"; import { Button } from "@/app/_components/ui/button"; import { Sheet, SheetContent, SheetHeader, SheetTitle, } from "@/app/_components/ui/sheet"; import { CartContext } from "@/app/_context/cart"; import { formatCurrency, calculateProductTotalPrice, } from "@/app/_helpers/price"; import { Prisma } from "@prisma/client"; import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; import Image from "next/image"; import { useContext, useState } from "react"; interface ProductDetailsProps { product: Prisma.ProductGetPayload<{ include: { restaurant: true; }; }>; complementaryProducts: Prisma.ProductGetPayload<{ include: { restaurant: true; }; }>[]; } const ProductDetails = ({ product, complementaryProducts, }: ProductDetailsProps) => { const [quantity, setQuantity] = useState(1); const [isCartOpen, setIsCartOpen] = useState(false); const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = useState(false); const { addProductToCart, products } = useContext(CartContext); const addToCart = ({ emptyCart }: { emptyCart?: boolean }) => { addProductToCart({ product: { ...product, quantity }, emptyCart }); setIsCartOpen(true); }; const handleAddToCartClick = () => { // VERIFICAR SE HÁ ALGUM PRODUTO DE OUTRO RESTAURANTE NO CARRINHO const hasDifferentRestaurantProduct = products.some( (cartProduct) => cartProduct.restaurantId !== product.restaurantId, ); // SE HOUVER, ABRIR UM AVISO if (hasDifferentRestaurantProduct) { return setIsConfirmationDialogOpen(true); } addToCart({ emptyCart: false, }); }; const handleIncreaseQuantityClick = () => setQuantity((currentState) => currentState + 1); const handleDecreaseQuantityClick = () => setQuantity((currentState) => { if (currentState === 1) return 1; return currentState - 1; }); return ( <>
{/* RESTAURANTE */}
{product.restaurant.name}
{product.restaurant.name}
{/* NOME DO PRODUTO */}

{product.name}

{/* PREÇO DO PRODUTO E QUANTIDADE */}
{/* PREÇO COM DESCONTO */}

{formatCurrency(calculateProductTotalPrice(product))}

{product.discountPercentage > 0 && ( )}
{/* PREÇO ORIGINAL */} {product.discountPercentage > 0 && (

De: {formatCurrency(Number(product.price))}

)}
{/* QUANTIDADE */}
{quantity}

Sobre

{product.description}

Sucos

Sacola Você só pode adicionar itens de um restaurante por vez Deseja mesmo adicionar esse produto? Isso limpará sua sacola atual. Cancelar addToCart({ emptyCart: true })}> Esvaziar sacola e adicionar ); }; export default ProductDetails; ================================================ FILE: app/products/[id]/_components/product-image.tsx ================================================ "use client"; import { Button } from "@/app/_components/ui/button"; import { Product } from "@prisma/client"; import { ChevronLeftIcon } from "lucide-react"; import Image from "next/image"; import { useRouter } from "next/navigation"; interface ProductImageProps { product: Pick; } const ProductImage = ({ product }: ProductImageProps) => { const router = useRouter(); const handleBackClick = () => router.back(); return (
{product.name}
); }; export default ProductImage; ================================================ FILE: app/products/[id]/page.tsx ================================================ import { db } from "@/app/_lib/prisma"; import { notFound } from "next/navigation"; import ProductImage from "./_components/product-image"; import ProductDetails from "./_components/product-details"; interface ProductPageProps { params: { id: string; }; } const ProductPage = async ({ params: { id } }: ProductPageProps) => { const product = await db.product.findUnique({ where: { id, }, include: { restaurant: true, }, }); if (!product) { return notFound(); } const juices = await db.product.findMany({ where: { category: { name: "Sucos", }, restaurant: { id: product?.restaurant.id, }, }, include: { restaurant: true, }, }); return (
{/* IMAGEM */} {/* TITULO E PREÇO */}
); }; export default ProductPage; ================================================ FILE: app/products/recommended/page.tsx ================================================ import Header from "@/app/_components/header"; import ProductItem from "@/app/_components/product-item"; import { db } from "@/app/_lib/prisma"; const RecommendedProductsPage = async () => { const products = await db.product.findMany({ where: { discountPercentage: { gt: 0, }, }, take: 20, include: { restaurant: { select: { name: true, }, }, }, }); // TODO: pegar produtos com mais pedidos return ( <>

Pedidos Recomendados

{products.map((product) => ( ))}
); }; export default RecommendedProductsPage; ================================================ FILE: app/restaurants/[id]/_components/cart-banner.tsx ================================================ "use client"; import Cart from "@/app/_components/cart"; import { Button } from "@/app/_components/ui/button"; import { Sheet, SheetContent, SheetHeader, SheetTitle, } from "@/app/_components/ui/sheet"; import { CartContext } from "@/app/_context/cart"; import { formatCurrency } from "@/app/_helpers/price"; import { Restaurant } from "@prisma/client"; import { useContext, useState } from "react"; interface CartBannerProps { restaurant: Pick; } const CartBanner = ({ restaurant }: CartBannerProps) => { const [isCartOpen, setIsCartOpen] = useState(false); const { products, totalPrice, totalQuantity } = useContext(CartContext); const restaurantHasProductsOnCart = products.some( (product) => product.restaurantId === restaurant.id, ); if (!restaurantHasProductsOnCart) return null; return (
{/* PREÇO */}
Total sem entrega

{formatCurrency(totalPrice)}{" "} {" "} / {totalQuantity} {totalQuantity > 1 ? "itens" : "item"}

{/* BOTÃO */} Sacola
); }; export default CartBanner; ================================================ FILE: app/restaurants/[id]/_components/restaurant-image.tsx ================================================ "use client"; import { Button } from "@/app/_components/ui/button"; import { isRestaurantFavorited } from "@/app/_helpers/restaurant"; import useToggleFavoriteRestaurant from "@/app/_hooks/use-toggle-favorite-restaurant"; import { Restaurant, UserFavoriteRestaurant } from "@prisma/client"; import { ChevronLeftIcon, HeartIcon } from "lucide-react"; import { useSession } from "next-auth/react"; import Image from "next/image"; import { useRouter } from "next/navigation"; interface RestaurantImageProps { restaurant: Pick; userFavoriteRestaurants: UserFavoriteRestaurant[]; } const RestaurantImage = ({ restaurant, userFavoriteRestaurants, }: RestaurantImageProps) => { const { data } = useSession(); const router = useRouter(); const isFavorite = isRestaurantFavorited( restaurant.id, userFavoriteRestaurants, ); const { handleFavoriteClick } = useToggleFavoriteRestaurant({ restaurantId: restaurant.id, userId: data?.user.id, restaurantIsFavorited: isFavorite, }); const handleBackClick = () => router.back(); return (
{restaurant.name}
); }; export default RestaurantImage; ================================================ FILE: app/restaurants/[id]/page.tsx ================================================ import { db } from "@/app/_lib/prisma"; import { notFound } from "next/navigation"; import RestaurantImage from "./_components/restaurant-image"; import Image from "next/image"; import { StarIcon } from "lucide-react"; import DeliveryInfo from "@/app/_components/delivery-info"; import ProductList from "@/app/_components/product-list"; import CartBanner from "./_components/cart-banner"; import { getServerSession } from "next-auth"; import { authOptions } from "@/app/_lib/auth"; interface RestaurantPageProps { params: { id: string; }; } const RestaurantPage = async ({ params: { id } }: RestaurantPageProps) => { const restaurant = await db.restaurant.findUnique({ where: { id, }, include: { categories: { orderBy: { createdAt: "desc", }, include: { products: { where: { restaurantId: id, }, include: { restaurant: { select: { name: true, }, }, }, }, }, }, products: { take: 10, include: { restaurant: { select: { name: true, }, }, }, }, }, }); if (!restaurant) { return notFound(); } const session = await getServerSession(authOptions); const userFavoriteRestaurants = await db.userFavoriteRestaurant.findMany({ where: { userId: session?.user.id, }, }); return (
{/* TITULO */}
{restaurant.name}

{restaurant.name}

5.0
{restaurant.categories.map((category) => (
{category.name}
))}
{/* TODO: mostrar produtos mais pedidos quando implementarmos realização de pedido */}

Mais Pedidos

{restaurant.categories.map((category) => (
{/* TODO: mostrar produtos mais pedidos quando implementarmos realização de pedido */}

{category.name}

))}
); }; export default RestaurantPage; ================================================ FILE: app/restaurants/_actions/search.ts ================================================ "use server"; import { db } from "@/app/_lib/prisma"; export const searchForRestaurants = async (search: string) => { const restaurants = await db.restaurant.findMany({ where: { name: { contains: search, mode: "insensitive", }, }, }); return restaurants; }; ================================================ FILE: app/restaurants/_components/restaurants.tsx ================================================ "use client"; import { Restaurant, UserFavoriteRestaurant } from "@prisma/client"; import { notFound, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import { searchForRestaurants } from "../_actions/search"; import Header from "@/app/_components/header"; import RestaurantItem from "@/app/_components/restaurant-item"; interface RestaurantProps { userFavoriteRestaurants: UserFavoriteRestaurant[]; } const Restaurants = ({ userFavoriteRestaurants }: RestaurantProps) => { const searchParams = useSearchParams(); const [restaurants, setRestaurants] = useState([]); const searchFor = searchParams.get("search"); useEffect(() => { const fetchRestaurants = async () => { if (!searchFor) return; const foundRestaurants = await searchForRestaurants(searchFor); setRestaurants(foundRestaurants); }; fetchRestaurants(); }, [searchFor]); if (!searchFor) { return notFound(); } return ( <>

Restaurantes Encontrados

{restaurants.map((restaurant) => ( ))}
); }; export default Restaurants; ================================================ FILE: app/restaurants/page.tsx ================================================ import { Suspense } from "react"; import Restaurants from "./_components/restaurants"; import { getServerSession } from "next-auth"; import { authOptions } from "../_lib/auth"; import { db } from "../_lib/prisma"; const RestaurantsPage = async () => { const session = await getServerSession(authOptions); const userFavoriteRestaurants = await db.userFavoriteRestaurant.findMany({ where: { userId: session?.user.id, }, include: { restaurant: true, }, }); return ( ); }; export default RestaurantsPage; ================================================ FILE: app/restaurants/recommended/page.tsx ================================================ import Header from "@/app/_components/header"; import RestaurantItem from "@/app/_components/restaurant-item"; import { authOptions } from "@/app/_lib/auth"; import { db } from "@/app/_lib/prisma"; import { getServerSession } from "next-auth"; const RecommendedRestaurants = async () => { const session = await getServerSession(authOptions); const userFavoriteRestaurants = await db.userFavoriteRestaurant.findMany({ where: { userId: session?.user.id, }, include: { restaurant: true, }, }); const restaurants = await db.restaurant.findMany({}); return ( <>

Restaurantes Recomendados

{restaurants.map((restaurant) => ( ))}
); }; export default RecommendedRestaurants; ================================================ FILE: components.json ================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": true, "tsx": true, "tailwind": { "config": "tailwind.config.ts", "css": "app/globals.css", "baseColor": "slate", "cssVariables": true, "prefix": "" }, "aliases": { "components": "@/app/_components", "utils": "@/app/_lib/utils" } } ================================================ FILE: docker-compose.yml ================================================ version: "3" services: postgres: image: postgres:latest container_name: fsw-foods-postgres environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password ports: - "5432:5432" volumes: - ./.postgres-data:/var/lib/postgresql/data volumes: .postgres-data: ================================================ FILE: next-auth.d.ts ================================================ /* eslint-disable no-unused-vars */ import { DefaultSession } from "next-auth"; declare module "next-auth" { interface Session { user: { id?: string; } & DefaultSession["user"]; } } ================================================ FILE: next.config.mjs ================================================ /** @type {import('next').NextConfig} */ const nextConfig = { images: { remotePatterns: [{ hostname: "utfs.io" }], }, }; export default nextConfig; ================================================ FILE: package.json ================================================ { "name": "fsw-foods", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "prepare": "husky && prisma generate" }, "prisma": { "seed": "ts-node ./prisma/seed.ts" }, "dependencies": { "@auth/prisma-adapter": "^2.0.0", "@prisma/client": "^5.12.1", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "git-commit-msg-linter": "^5.0.7", "lucide-react": "^0.368.0", "next": "14.2.1", "next-auth": "^4.24.7", "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18", "sonner": "^1.4.41", "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "eslint": "^8", "eslint-config-next": "14.2.1", "husky": "^9.0.11", "lint-staged": "^15.2.2", "postcss": "^8", "prettier": "^3.2.5", "prettier-plugin-tailwindcss": "^0.5.14", "prisma": "^5.12.1", "tailwindcss": "^3.4.1", "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/20240415210903_init_database/migration.sql ================================================ -- CreateTable CREATE TABLE "Restaurant" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, "imageUrl" TEXT NOT NULL, "deliveryFee" DECIMAL(10,2) NOT NULL, "deliveryTimeMinutes" INTEGER NOT NULL, CONSTRAINT "Restaurant_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Category" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, "imageUrl" TEXT NOT NULL, CONSTRAINT "Category_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Product" ( "id" TEXT NOT NULL, "name" TEXT NOT NULL, "description" TEXT NOT NULL, "imageUrl" TEXT NOT NULL, "price" DECIMAL(10,2) NOT NULL, "discountPercentage" INTEGER NOT NULL DEFAULT 0, "restaurantId" TEXT NOT NULL, "categoryId" TEXT NOT NULL, CONSTRAINT "Product_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "_CategoryToRestaurant" ( "A" TEXT NOT NULL, "B" TEXT NOT NULL ); -- CreateIndex CREATE UNIQUE INDEX "_CategoryToRestaurant_AB_unique" ON "_CategoryToRestaurant"("A", "B"); -- CreateIndex CREATE INDEX "_CategoryToRestaurant_B_index" ON "_CategoryToRestaurant"("B"); -- AddForeignKey ALTER TABLE "Product" ADD CONSTRAINT "Product_restaurantId_fkey" FOREIGN KEY ("restaurantId") REFERENCES "Restaurant"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Product" ADD CONSTRAINT "Product_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "_CategoryToRestaurant" ADD CONSTRAINT "_CategoryToRestaurant_A_fkey" FOREIGN KEY ("A") REFERENCES "Category"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "_CategoryToRestaurant" ADD CONSTRAINT "_CategoryToRestaurant_B_fkey" FOREIGN KEY ("B") REFERENCES "Restaurant"("id") ON DELETE CASCADE ON UPDATE CASCADE; ================================================ FILE: prisma/migrations/20240501004806_add_created_at/migration.sql ================================================ -- AlterTable ALTER TABLE "Category" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; -- AlterTable ALTER TABLE "Product" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; ================================================ FILE: prisma/migrations/20240502232158_add_auth_tables/migration.sql ================================================ -- CreateTable CREATE TABLE "accounts" ( "id" TEXT NOT NULL, "user_id" TEXT NOT NULL, "type" TEXT NOT NULL, "provider" TEXT NOT NULL, "provider_account_id" TEXT NOT NULL, "refresh_token" TEXT, "access_token" TEXT, "expires_at" INTEGER, "token_type" TEXT, "scope" TEXT, "id_token" TEXT, "session_state" TEXT, CONSTRAINT "accounts_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "sessions" ( "id" TEXT NOT NULL, "session_token" TEXT NOT NULL, "user_id" TEXT NOT NULL, "expires" TIMESTAMP(3) NOT NULL, CONSTRAINT "sessions_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "users" ( "id" TEXT NOT NULL, "name" TEXT, "email" TEXT, "email_verified" TIMESTAMP(3), "image" TEXT, CONSTRAINT "users_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "verificationtokens" ( "identifier" TEXT NOT NULL, "token" TEXT NOT NULL, "expires" TIMESTAMP(3) NOT NULL ); -- CreateIndex CREATE UNIQUE INDEX "accounts_provider_provider_account_id_key" ON "accounts"("provider", "provider_account_id"); -- CreateIndex CREATE UNIQUE INDEX "sessions_session_token_key" ON "sessions"("session_token"); -- CreateIndex CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); -- CreateIndex CREATE UNIQUE INDEX "verificationtokens_identifier_token_key" ON "verificationtokens"("identifier", "token"); -- AddForeignKey ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; ================================================ FILE: prisma/migrations/20240503000412_add_order_table/migration.sql ================================================ -- CreateEnum CREATE TYPE "OrderStatus" AS ENUM ('CONFIRMED', 'CANCELED', 'PREPARING', 'DELIVERING', 'COMPLETED'); -- CreateTable CREATE TABLE "Order" ( "id" TEXT NOT NULL, "userId" TEXT NOT NULL, "restaurantId" TEXT NOT NULL, "deliveryFee" DECIMAL(10,2) NOT NULL, "deliveryTimeMinutes" INTEGER NOT NULL, "subtotalPrice" DECIMAL(10,2) NOT NULL, "totalPrice" DECIMAL(10,2) NOT NULL, "totalDiscounts" DECIMAL(10,2) NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "status" "OrderStatus" NOT NULL, CONSTRAINT "Order_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "_OrderToProduct" ( "A" TEXT NOT NULL, "B" TEXT NOT NULL ); -- CreateIndex CREATE UNIQUE INDEX "_OrderToProduct_AB_unique" ON "_OrderToProduct"("A", "B"); -- CreateIndex CREATE INDEX "_OrderToProduct_B_index" ON "_OrderToProduct"("B"); -- AddForeignKey ALTER TABLE "Order" ADD CONSTRAINT "Order_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Order" ADD CONSTRAINT "Order_restaurantId_fkey" FOREIGN KEY ("restaurantId") REFERENCES "Restaurant"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "_OrderToProduct" ADD CONSTRAINT "_OrderToProduct_A_fkey" FOREIGN KEY ("A") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "_OrderToProduct" ADD CONSTRAINT "_OrderToProduct_B_fkey" FOREIGN KEY ("B") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; ================================================ FILE: prisma/migrations/20240503012547_add_order_product_table/migration.sql ================================================ /* Warnings: - You are about to drop the `_OrderToProduct` table. If the table is not empty, all the data it contains will be lost. */ -- DropForeignKey ALTER TABLE "_OrderToProduct" DROP CONSTRAINT "_OrderToProduct_A_fkey"; -- DropForeignKey ALTER TABLE "_OrderToProduct" DROP CONSTRAINT "_OrderToProduct_B_fkey"; -- AlterTable ALTER TABLE "Order" ADD COLUMN "productId" TEXT; -- DropTable DROP TABLE "_OrderToProduct"; -- CreateTable CREATE TABLE "OrderProduct" ( "id" TEXT NOT NULL, "orderId" TEXT NOT NULL, "productId" TEXT NOT NULL, "quantity" INTEGER NOT NULL, CONSTRAINT "OrderProduct_pkey" PRIMARY KEY ("id") ); -- AddForeignKey ALTER TABLE "OrderProduct" ADD CONSTRAINT "OrderProduct_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "OrderProduct" ADD CONSTRAINT "OrderProduct_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Order" ADD CONSTRAINT "Order_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE; ================================================ FILE: prisma/migrations/20240503012822_add_order_product_table/migration.sql ================================================ /* Warnings: - You are about to drop the column `productId` on the `Order` table. All the data in the column will be lost. */ -- DropForeignKey ALTER TABLE "Order" DROP CONSTRAINT "Order_productId_fkey"; -- AlterTable ALTER TABLE "Order" DROP COLUMN "productId"; ================================================ FILE: prisma/migrations/20240503233901_add_user_restaurant_favorites_table/migration.sql ================================================ -- CreateTable CREATE TABLE "UserFavoriteRestaurant" ( "id" TEXT NOT NULL, "userId" TEXT NOT NULL, "restaurantId" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "UserFavoriteRestaurant_pkey" PRIMARY KEY ("id") ); -- AddForeignKey ALTER TABLE "UserFavoriteRestaurant" ADD CONSTRAINT "UserFavoriteRestaurant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "UserFavoriteRestaurant" ADD CONSTRAINT "UserFavoriteRestaurant_restaurantId_fkey" FOREIGN KEY ("restaurantId") REFERENCES "Restaurant"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ================================================ FILE: prisma/migrations/20240503235713_add_compound_key_to_user_favorite_restaurant/migration.sql ================================================ /* Warnings: - The primary key for the `UserFavoriteRestaurant` table will be changed. If it partially fails, the table could be left without primary key constraint. - You are about to drop the column `id` on the `UserFavoriteRestaurant` table. All the data in the column will be lost. */ -- AlterTable ALTER TABLE "UserFavoriteRestaurant" DROP CONSTRAINT "UserFavoriteRestaurant_pkey", DROP COLUMN "id", ADD CONSTRAINT "UserFavoriteRestaurant_pkey" PRIMARY KEY ("userId", "restaurantId"); ================================================ 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" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model Account { id String @id @default(cuid()) userId String @map("user_id") type String provider String providerAccountId String @map("provider_account_id") refresh_token String? @db.Text access_token String? @db.Text expires_at Int? token_type String? scope String? id_token String? @db.Text session_state String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) @@map("accounts") } model Session { id String @id @default(cuid()) sessionToken String @unique @map("session_token") userId String @map("user_id") expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("sessions") } model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? @map("email_verified") image String? accounts Account[] sessions Session[] orders Order[] favoriteRestaurants UserFavoriteRestaurant[] @@map("users") } model VerificationToken { identifier String token String expires DateTime @@unique([identifier, token]) @@map("verificationtokens") } model Restaurant { id String @id @default(uuid()) name String imageUrl String deliveryFee Decimal @db.Decimal(10, 2) deliveryTimeMinutes Int categories Category[] products Product[] orders Order[] usersWhoFavorited UserFavoriteRestaurant[] } model UserFavoriteRestaurant { userId String user User @relation(fields: [userId], references: [id]) restaurantId String restaurant Restaurant @relation(fields: [restaurantId], references: [id]) createdAt DateTime @default(now()) @@id([userId, restaurantId]) } model Category { id String @id @default(uuid()) name String imageUrl String restaurants Restaurant[] products Product[] createdAt DateTime @default(now()) } model Product { id String @id @default(uuid()) name String description String imageUrl String price Decimal @db.Decimal(10, 2) discountPercentage Int @default(0) restaurantId String restaurant Restaurant @relation(fields: [restaurantId], references: [id]) categoryId String category Category @relation(fields: [categoryId], references: [id]) createdAt DateTime @default(now()) orderProducts OrderProduct[] } model OrderProduct { id String @id @default(uuid()) orderId String order Order @relation(fields: [orderId], references: [id]) productId String product Product @relation(fields: [productId], references: [id]) quantity Int } model Order { id String @id @default(uuid()) userId String user User @relation(fields: [userId], references: [id]) products OrderProduct[] restaurant Restaurant @relation(fields: [restaurantId], references: [id]) restaurantId String deliveryFee Decimal @db.Decimal(10, 2) deliveryTimeMinutes Int subtotalPrice Decimal @db.Decimal(10, 2) totalPrice Decimal @db.Decimal(10, 2) totalDiscounts Decimal @db.Decimal(10, 2) createdAt DateTime @default(now()) status OrderStatus } enum OrderStatus { CONFIRMED CANCELED PREPARING DELIVERING COMPLETED } ================================================ FILE: prisma/seed.ts ================================================ const { PrismaClient } = require("@prisma/client"); const prismaClient = new PrismaClient(); const description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean nec nisl lorem. Praesent pharetra, sapien ut fringilla malesuada, nisi felis ullamcorper ex, eu consectetur elit dolor sed dolor. Praesent orci mi, auctor aliquet semper vitae, volutpat quis augue. Cras porta sapien nec pharetra laoreet. Sed at velit sit amet mauris varius volutpat sit amet id mauris. Maecenas vitae mattis ante. Morbi nulla quam, sagittis at orci eu, scelerisque auctor neque."; const createBurguers = async ( desertsCategoryId: string, juicesCategoryId: string, ) => { const burguersCategory = await prismaClient.category.create({ data: { name: "Hambúrgueres", imageUrl: "https://utfs.io/f/92918634-fc03-4425-bc1f-d1fbc8933586-vzk6us.png", }, }); const burguerRestaurants = [ { name: "The Burguer King", imageUrl: "https://utfs.io/f/020e448e-a7d8-433f-9622-cb3b68f34d48-p3apya.png", deliveryFee: 5, deliveryTimeMinutes: 30, categories: { connect: { id: burguersCategory.id, }, }, }, { name: "Omni Burguer", imageUrl: "https://utfs.io/f/d0c54665-78d0-41af-98a4-8d1f459c622c-p3apy9.png", deliveryFee: 5, deliveryTimeMinutes: 30, categories: { connect: { id: burguersCategory.id, }, }, }, { name: "The Burguer Queen", imageUrl: "https://utfs.io/f/d9834f2e-bc37-4c64-981b-cabf03018322-p3apy8.png", deliveryFee: 0, deliveryTimeMinutes: 45, categories: { connect: { id: burguersCategory.id, }, }, }, { name: "Burguer House", imageUrl: "https://utfs.io/f/9c193fc1-9dcb-4394-8be4-d783266134dc-p3apy7.png", deliveryFee: 10, deliveryTimeMinutes: 20, categories: { connect: { id: burguersCategory.id, }, }, }, ]; for (const item of burguerRestaurants) { const restaurant = await prismaClient.restaurant.create({ data: item, }); await createDeserts(restaurant.id, desertsCategoryId); await createJuices(restaurant.id, juicesCategoryId); console.log(`Created ${restaurant.name}`); const burguerProducts = [ { name: "Cheese Burguer", price: 30, description: description, discountPercentage: 10, imageUrl: "https://utfs.io/f/ae177fa1-129c-4f43-9928-aa8ac1080a18-yqapzx.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: burguersCategory.id, }, }, }, { name: "Double Cheese Burguer", price: 40, description: description, discountPercentage: 7, imageUrl: "https://utfs.io/f/dca007fe-0025-422e-9328-16d40f0a1792-yqapzy.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: burguersCategory.id, }, }, }, { name: "Bacon Burguer", price: 35, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/4cb1ca21-0748-4296-a23d-88e52687506a-yqapzz.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: burguersCategory.id, }, }, }, { name: "Double Bacon Burguer", price: 45, description: description, discountPercentage: 10, imageUrl: "https://utfs.io/f/ed9fde1e-0675-4829-8001-a775e2825dc6-yqaq00.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: burguersCategory.id, }, }, }, { name: "Chicken Burguer", price: 30, description: description, discountPercentage: 7, imageUrl: "https://utfs.io/f/0aff860a-3e05-42fd-9b2a-53d03c744949-yqaq01.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: burguersCategory.id, }, }, }, { name: "Double Chicken Burguer", price: 40, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/d2157790-fcb7-4d09-b074-80af4bfb9892-yqaq02.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: burguersCategory.id, }, }, }, ]; for (const product of burguerProducts) { await prismaClient.product.create({ data: product, }); console.log(`Created ${product.name}`); } } }; const createPizzas = async ( desertsCategoryId: string, juicesCategoryId: string, ) => { const pizzasCategory = await prismaClient.category.create({ data: { name: "Pizzas", imageUrl: "https://utfs.io/f/d9ca0163-6bc8-42dc-bbb3-377636849cd8-mtj7yz.png", }, }); const pizzaRestaurants = [ { name: "Pizza Hut", imageUrl: "https://utfs.io/f/f50301c9-7968-4d76-b4a3-b8ed24e2089c-5p2j0.png", deliveryFee: 5, deliveryTimeMinutes: 30, categories: { connect: { id: pizzasCategory.id, }, }, }, { name: "Omni Pizza", imageUrl: "https://utfs.io/f/8a9eb9dc-6434-4246-91c9-1c0a60a6e5f0-5p2j1.png", deliveryFee: 5, deliveryTimeMinutes: 30, categories: { connect: { id: pizzasCategory.id, }, }, }, { name: "The Pizza Queen", imageUrl: "https://utfs.io/f/e83dc871-19e3-4d39-8163-fb2f1e24b6b1-5p2j2.png", deliveryFee: 0, deliveryTimeMinutes: 45, categories: { connect: { id: pizzasCategory.id, }, }, }, { name: "Pizza House", imageUrl: "https://utfs.io/f/a73ec63a-7fc8-4a23-8d03-62debee79e6a-5p2j3.png", deliveryFee: 10, deliveryTimeMinutes: 20, categories: { connect: { id: pizzasCategory.id, }, }, }, ]; for (const item of pizzaRestaurants) { const restaurant = await prismaClient.restaurant.create({ data: item, }); await createDeserts(restaurant.id, desertsCategoryId); await createJuices(restaurant.id, juicesCategoryId); console.log(`Created ${restaurant.name}`); const pizzaProducts = [ { name: "Pepperoni Pizza", price: 45, description: description, discountPercentage: 0, imageUrl: "https://utfs.io/f/645ba997-00b1-44ed-9928-b9eb41e93896-berpub.jpg", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: pizzasCategory.id, }, }, }, { name: "Margarita Pizza", price: 40, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/4ee1f69b-e0a3-4166-bae5-b666996bcd3b-berpua.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: pizzasCategory.id, }, }, }, { name: "Hawaiian Pizza", price: 45, description: "A delicious hawaiian pizza", discountPercentage: 5, imageUrl: "https://utfs.io/f/0bb7a869-f369-4506-94ea-6cc23c8dd92f-berpu9.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: pizzasCategory.id, }, }, }, { name: "Vegetarian Pizza", price: 35, description: description, discountPercentage: 0, imageUrl: "https://utfs.io/f/1bb04a24-361c-4e3a-ad2f-81255f2d53b9-berpux.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: pizzasCategory.id, }, }, }, { name: "Meat Lovers Pizza", price: 50, description: description, discountPercentage: 10, imageUrl: "https://utfs.io/f/ead919ee-2e3d-423f-b294-e525f9d6a5b7-berpuy.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: pizzasCategory.id, }, }, }, ]; for (const product of pizzaProducts) { await prismaClient.product.create({ data: product, }); console.log(`Created ${product.name}`); } } }; const createJapanese = async ( desertsCategoryId: string, juicesCategoryId: string, ) => { const japaneseCategory = await prismaClient.category.create({ data: { name: "Japonesa", imageUrl: "https://utfs.io/f/ccc2351a-49b0-4613-a233-3b3b3bd6a47c-yd9ii3.png", }, }); const japaneseRestaurants = [ { name: "Sushi House", imageUrl: "https://utfs.io/f/7f52b936-9f7a-40cc-b22f-b62727ddb9cc-fu3r05.png", deliveryFee: 5, deliveryTimeMinutes: 30, categories: { connect: { id: japaneseCategory.id, }, }, }, { name: "Omni Sushi", imageUrl: "https://utfs.io/f/f809b477-7cf1-47f5-8664-0a4566225867-fu3r06.png", deliveryFee: 5, deliveryTimeMinutes: 30, categories: { connect: { id: japaneseCategory.id, }, }, }, { name: "The Sushi Queen", imageUrl: "https://utfs.io/f/42bb722a-0b76-40e8-8251-cee9093bed38-fu3r07.png", deliveryFee: 0, deliveryTimeMinutes: 45, categories: { connect: { id: japaneseCategory.id, }, }, }, { name: "Sushi House", imageUrl: "https://utfs.io/f/de37be82-23bf-4901-aeea-b93c281bf401-fu3r08.png", deliveryFee: 10, deliveryTimeMinutes: 20, categories: { connect: { id: japaneseCategory.id, }, }, }, ]; for (const item of japaneseRestaurants) { const restaurant = await prismaClient.restaurant.create({ data: item, }); console.log(`Created ${restaurant.name}`); await createDeserts(restaurant.id, desertsCategoryId); await createJuices(restaurant.id, juicesCategoryId); const japaneseProducts = [ { name: "Sushi Combo", price: 30, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/5ef70d5c-892b-424d-8655-6bc2716411e1-1lryd0.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: japaneseCategory.id, }, }, }, { name: "Sashimi Combo", price: 40, description: description, discountPercentage: 10, imageUrl: "https://utfs.io/f/e8b2fb18-d636-477f-8bed-cfe85358246f-1lryd1.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: japaneseCategory.id, }, }, }, { name: "Nigiri Combo", price: 35, description: description, discountPercentage: 7, imageUrl: "https://utfs.io/f/fd9458a3-153b-4833-aca1-61a882da1ce6-1lryd2.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: japaneseCategory.id, }, }, }, { name: "Temaki Combo", price: 45, description: description, discountPercentage: 0, imageUrl: "https://utfs.io/f/eec36a13-de2d-48ed-92d2-4f74477dad83-1lryd3.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: japaneseCategory.id, }, }, }, { name: "Uramaki Combo", price: 30, description: description, discountPercentage: 10, imageUrl: "https://utfs.io/f/c04a5df1-c1ac-4e28-ba48-27d856caa553-1lryd4.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: japaneseCategory.id, }, }, }, { name: "Hosomaki Combo", price: 40, description: description, discountPercentage: 0, imageUrl: "https://utfs.io/f/fd147569-14c6-428d-9a54-df64c61c6bb6-1lryd5.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: japaneseCategory.id, }, }, }, ]; for (const product of japaneseProducts) { await prismaClient.product.create({ data: product, }); console.log(`Created ${product.name}`); } } }; const createBrazilian = async ( desertsCategoryId: string, juicesCategoryId: string, ) => { const brazilianCategory = await prismaClient.category.create({ data: { name: "Brasileira", imageUrl: "https://utfs.io/f/d84e3a7a-fcf6-4d3d-86bf-d62c0b1febdc-m1yv44.png", }, }); const brazilianRestaurants = [ { name: "Churrascaria House", imageUrl: "https://utfs.io/f/5a090f6e-520f-418a-a42a-043b512314a2-n9n78u.png", deliveryFee: 5, deliveryTimeMinutes: 30, categories: { connect: { id: brazilianCategory.id, }, }, }, { name: "Omni Churrascaria", imageUrl: "https://utfs.io/f/87338583-660e-47f1-a80d-6ea804298bd5-n9n78v.png", deliveryFee: 5, deliveryTimeMinutes: 30, categories: { connect: { id: brazilianCategory.id, }, }, }, { name: "The Churrascaria Queen", imageUrl: "https://utfs.io/f/b26b00ca-5041-46cb-9b68-a1856ed064ad-n9n78w.png", deliveryFee: 0, deliveryTimeMinutes: 45, categories: { connect: { id: brazilianCategory.id, }, }, }, { name: "Churrascaria House", imageUrl: "https://utfs.io/f/c1f279ea-ac09-4e4f-9757-30018cb4c7bc-n9n78x.png", deliveryFee: 10, deliveryTimeMinutes: 20, categories: { connect: { id: brazilianCategory.id, }, }, }, ]; for (const item of brazilianRestaurants) { const restaurant = await prismaClient.restaurant.create({ data: item, }); console.log(`Created ${restaurant.name}`); await createDeserts(restaurant.id, desertsCategoryId); await createJuices(restaurant.id, juicesCategoryId); const brazilianProducts = [ { name: "Camarão Citrus", price: 40, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/cecdeeb8-10e6-4be8-8553-0a120717d194-xf34p9.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: brazilianCategory.id, }, }, }, { name: "Picanha Especial", price: 45, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/089299df-fcb9-446a-a8cc-75e4e26b7357-xf34p8.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: brazilianCategory.id, }, }, }, { name: "Macarrão com Carne", price: 35, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/891eb8aa-635e-4cb3-b7fd-eb8d1c9f14e1-xf34p7.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: brazilianCategory.id, }, }, }, { name: "Carne com Salada", price: 35, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/43d9e18a-4ba9-47b6-9a87-6d4fedbd6f41-xf34ol.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: brazilianCategory.id, }, }, }, { name: "Filé Mignon com Fritas", price: 40, description: description, discountPercentage: 0, imageUrl: "https://utfs.io/f/0cfa51a6-1a88-4114-a6c6-bf607a5a1cb0-xf34ok.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: brazilianCategory.id, }, }, }, { name: "Frango ao Molho", price: 40, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/9158a622-4b87-4ec6-a726-569dee27a093-xf34oj.png", restaurant: { connect: { id: restaurant.id, }, }, category: { connect: { id: brazilianCategory.id, }, }, }, ]; for (const product of brazilianProducts) { await prismaClient.product.create({ data: product, }); console.log(`Created ${product.name}`); } } }; const createDeserts = async (restaurantId: string, categoryId: string) => { await prismaClient.restaurant.update({ where: { id: restaurantId, }, data: { categories: { connect: { id: categoryId, }, }, }, }); const desertProducts = [ { name: "Sorvete Especial", price: 30, description: description, discountPercentage: 10, imageUrl: "https://utfs.io/f/b703fcaa-eb9c-4257-a08e-fba0f0e12fc1-pr8gxl.png", restaurant: { connect: { id: restaurantId, }, }, category: { connect: { id: categoryId, }, }, }, { name: "Bolo de Chocolate", price: 40, description: description, discountPercentage: 7, imageUrl: "https://utfs.io/f/029befff-aba7-49b3-91c4-8da022e699b0-pr8gxm.png", restaurant: { connect: { id: restaurantId, }, }, category: { connect: { id: categoryId, }, }, }, { name: "Petit Gateau", price: 55, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/98f262f6-dc35-428b-bac9-ac443f9f41bb-pr8gxn.png", restaurant: { connect: { id: restaurantId, }, }, category: { connect: { id: categoryId, }, }, }, { name: "Bolo de Morango", price: 35, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/6e6ad97a-f1f1-4d4b-bb40-f5ff25ba97d4-pr8gxo.png", restaurant: { connect: { id: restaurantId, }, }, category: { connect: { id: categoryId, }, }, }, { name: "Biscoito de Chocolate", price: 30, description: description, discountPercentage: 7, imageUrl: "https://utfs.io/f/4b8d0b7c-daa9-46f6-aebd-385cf5e086f7-pr8gxp.png", restaurant: { connect: { id: restaurantId, }, }, category: { connect: { id: categoryId, }, }, }, { name: "Torta de Morango", price: 45, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/4caadde1-0a1c-45a6-895b-4bfb6986099d-pr8gxq.png", restaurant: { connect: { id: restaurantId, }, }, category: { connect: { id: categoryId, }, }, }, ]; for (const product of desertProducts) { await prismaClient.product.create({ data: product, }); console.log(`Created ${product.name}`); } }; const createJuices = async (restaurantId: string, categoryId: string) => { await prismaClient.restaurant.update({ where: { id: restaurantId, }, data: { categories: { connect: { id: categoryId, }, }, }, }); const juiceProducts = [ { name: "Suco de Cenoura", price: 15, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/5126e950-40ca-4ef1-a166-16274fec16bc-6b2vea.png", restaurant: { connect: { id: restaurantId, }, }, category: { connect: { id: categoryId, }, }, }, { name: "Suco Cítrico", price: 20, description: description, discountPercentage: 7, imageUrl: "https://utfs.io/f/6dbe915d-af87-4f2a-b841-864ba9427da8-6b2ve9.png", restaurant: { connect: { id: restaurantId, }, }, category: { connect: { id: categoryId, }, }, }, { name: "Suco de Limão", price: 12, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/03aa4137-c949-4d2c-bdf2-bad6dd1f565e-6b2ve7.png", restaurant: { connect: { id: restaurantId, }, }, category: { connect: { id: categoryId, }, }, }, { name: "Suco de Laranja", price: 12, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/ce2b8e30-b922-4b1e-bdde-656348cd25c3-6b2ve6.png", restaurant: { connect: { id: restaurantId, }, }, category: { connect: { id: categoryId, }, }, }, { name: "Suco de Abacaxi", price: 12, description: description, discountPercentage: 7, imageUrl: "https://utfs.io/f/c4202826-7014-4368-8941-fa1af9b9c8b2-6b2ve5.png", restaurant: { connect: { id: restaurantId, }, }, category: { connect: { id: categoryId, }, }, }, { name: "Suco de Melancia", price: 12, description: description, discountPercentage: 5, imageUrl: "https://utfs.io/f/a9ba878f-79a8-4c25-883c-5c2e1670b256-6b2ve4.png", restaurant: { connect: { id: restaurantId, }, }, category: { connect: { id: categoryId, }, }, }, ]; for (const product of juiceProducts) { await prismaClient.product.create({ data: product, }); console.log(`Created ${product.name}`); } }; const main = async () => { const desertsCategory = await prismaClient.category.create({ data: { name: "Sobremesas", imageUrl: "https://utfs.io/f/0f81c141-4787-4a81-abce-cbd9c6596c7a-xayf5d.png", }, }); const juicesCategory = await prismaClient.category.create({ data: { name: "Sucos", imageUrl: "https://utfs.io/f/9f3013bf-0778-4d80-a330-4da2682deaf9-o41y62.png", }, }); await createBurguers(desertsCategory.id, juicesCategory.id); await createPizzas(desertsCategory.id, juicesCategory.id); await createJapanese(desertsCategory.id, juicesCategory.id); await createBrazilian(desertsCategory.id, juicesCategory.id); }; main() .then(() => { console.log("Seed do banco de dados realizado com sucesso!"); }) .catch((error) => { console.error(error); process.exit(1); }) .finally(async () => { await prismaClient.$disconnect(); }); ================================================ FILE: tailwind.config.ts ================================================ import type { Config } from "tailwindcss" const config = { darkMode: ["class"], content: [ './pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}', ], prefix: "", theme: { container: { center: true, padding: "2rem", screens: { "2xl": "1400px", }, }, extend: { colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))", }, secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))", }, muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))", }, accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))", }, popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))", }, card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, keyframes: { "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", }, }, }, plugins: [require("tailwindcss-animate")], } satisfies Config export default config ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "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", "next-auth.d.ts" ], "exclude": ["node_modules"] }