main 2d12da30da96 cached
76 files
124.0 KB
33.7k tokens
48 symbols
1 requests
Download .txt
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 (
    <div className="flex items-center justify-between">
      <div className="flex items-center gap-4">
        {/* IMAGEM E INFO */}
        <div className="relative h-20 w-20">
          <Image
            src={cartProduct.imageUrl}
            alt={cartProduct.name}
            fill
            sizes="100%"
            className="rounded-lg object-cover"
          />
        </div>

        <div className="space-y-1">
          <h3 className="text-xs">{cartProduct.name}</h3>

          <div className="flex items-center gap-1">
            <h4 className="text-sm font-semibold">
              {formatCurrency(
                calculateProductTotalPrice(cartProduct) * cartProduct.quantity,
              )}
            </h4>
            {cartProduct.discountPercentage > 0 && (
              <span className="text-xs text-muted-foreground line-through">
                {formatCurrency(
                  Number(cartProduct.price) * cartProduct.quantity,
                )}
              </span>
            )}
          </div>

          {/* QUANTIDADE */}

          <div className="flex items-center text-center">
            <Button
              size="icon"
              variant="ghost"
              className="h-7 w-7 border border-solid border-muted-foreground"
            >
              <ChevronLeftIcon
                size={16}
                onClick={handleDecreaseQuantityClick}
              />
            </Button>
            <p className="block w-8 text-xs">{cartProduct.quantity}</p>
            <Button
              size="icon"
              className="h-7 w-7"
              onClick={handleIncreaseQuantityClick}
            >
              <ChevronRightIcon size={16} />
            </Button>
          </div>
        </div>
      </div>

      {/* BOTÃO DE DELETAR */}
      <Button
        size="icon"
        variant="ghost"
        className="h-7 w-7 border border-solid border-muted-foreground"
        onClick={handleRemoveClick}
      >
        <TrashIcon size={16} />
      </Button>
    </div>
  );
};

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 (
    <>
      <div className="flex h-full flex-col py-5">
        {products.length > 0 ? (
          <>
            <div className="flex-auto space-y-4">
              {products.map((product) => (
                <CartItem key={product.id} cartProduct={product} />
              ))}
            </div>

            {/* TOTAIS */}
            <div className="mt-6">
              <Card>
                <CardContent className="space-y-2 p-5">
                  <div className="flex items-center justify-between text-xs">
                    <span className="text-muted-foreground">Subtotal</span>
                    <span>{formatCurrency(subtotalPrice)}</span>
                  </div>

                  <Separator />

                  <div className="flex items-center justify-between text-xs">
                    <span className="text-muted-foreground">Descontos</span>
                    <span>- {formatCurrency(totalDiscounts)}</span>
                  </div>

                  <Separator className="h-[0.5px]" />

                  <div className="flex items-center justify-between text-xs">
                    <span className="text-muted-foreground">Entrega</span>

                    {Number(products?.[0].restaurant.deliveryFee) === 0 ? (
                      <span className="uppercase text-primary">Grátis</span>
                    ) : (
                      formatCurrency(
                        Number(products?.[0].restaurant.deliveryFee),
                      )
                    )}
                  </div>

                  <Separator />

                  <div className="flex items-center justify-between text-xs font-semibold">
                    <span>Total</span>
                    <span>{formatCurrency(totalPrice)}</span>
                  </div>
                </CardContent>
              </Card>
            </div>

            {/* FINALIZAR PEDIDO */}
            <Button
              className="mt-6 w-full"
              onClick={() => setIsConfirmDialogOpen(true)}
              disabled={isSubmitLoading}
            >
              Finalizar pedido
            </Button>
          </>
        ) : (
          <h2 className="text-left font-medium">Sua sacola está vazia.</h2>
        )}
      </div>

      <AlertDialog
        open={isConfirmDialogOpen}
        onOpenChange={setIsConfirmDialogOpen}
      >
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Deseja finalizar seu pedido?</AlertDialogTitle>
            <AlertDialogDescription>
              Ao finalizar seu pedido, você concorda com os termos e condições
              da nossa plataforma.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancelar</AlertDialogCancel>
            <AlertDialogAction
              onClick={handleFinishOrderClick}
              disabled={isSubmitLoading}
            >
              {isSubmitLoading && (
                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
              )}
              Finalizar
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  );
};

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 (
    <Link
      href={`/categories/${category.id}/products`}
      className="flex items-center justify-center gap-3 rounded-full bg-white px-4 py-3 shadow-md"
    >
      <Image
        src={category.imageUrl}
        alt={category.name}
        height={30}
        width={30}
      />

      <span className="text-sm font-semibold">{category.name}</span>
    </Link>
  );
};

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 (
    <div className="grid grid-cols-2 gap-3">
      {categories.map((category) => (
        <CategoryItem key={category.id} category={category} />
      ))}
    </div>
  );
};

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<Restaurant, "deliveryFee" | "deliveryTimeMinutes">;
}

const DeliveryInfo = ({ restaurant }: DeliveryInfoProps) => {
  return (
    <>
      <Card className="mt-6 flex justify-around py-3">
        {/* CUSTO */}
        <div className="flex flex-col items-center">
          <div className="flex items-center gap-1 text-muted-foreground">
            <span className="text-xs">Entrega</span>
            <BikeIcon size={14} />
          </div>

          {Number(restaurant.deliveryFee) > 0 ? (
            <p className="text-xs font-semibold">
              {formatCurrency(Number(restaurant.deliveryFee))}
            </p>
          ) : (
            <p className="text-xs font-semibold">Grátis</p>
          )}
        </div>

        {/* TEMPO */}
        <div className="flex flex-col items-center">
          <div className="flex items-center gap-1 text-muted-foreground">
            <span className="text-xs">Entrega</span>
            <TimerIcon size={14} />
          </div>

          <p className="text-xs font-semibold">
            {restaurant.deliveryTimeMinutes} min
          </p>
        </div>
      </Card>
    </>
  );
};

export default DeliveryInfo;


================================================
FILE: app/_components/discount-badge.tsx
================================================
import { Product } from "@prisma/client";
import { ArrowDownIcon } from "lucide-react";

interface DiscountBadgeProps {
  product: Pick<Product, "discountPercentage">;
}

const DiscountBadge = ({ product }: DiscountBadgeProps) => {
  return (
    <div className="flex items-center gap-[2px] rounded-full bg-primary px-2 py-[2px] text-white">
      <ArrowDownIcon size={12} />
      <span className="text-xs font-semibold">
        {product.discountPercentage}%
      </span>
    </div>
  );
};

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 (
    <div className="flex justify-between px-5 pt-6">
      <Link href="/">
        <div className="relative h-[30px] w-[100px]">
          <Image
            src="/logo.png"
            alt="FSW Foods"
            sizes="100%"
            fill
            className="object-cover"
          />
        </div>
      </Link>

      <Sheet>
        <SheetTrigger asChild>
          <Button
            size="icon"
            variant="outline"
            className="border-none bg-transparent"
          >
            <MenuIcon />
          </Button>
        </SheetTrigger>

        <SheetContent>
          <SheetHeader>
            <SheetTitle className="text-left">Menu</SheetTitle>
          </SheetHeader>

          {data?.user ? (
            <>
              <div className="flex justify-between pt-6">
                <div className="flex items-center gap-3">
                  <Avatar>
                    <AvatarImage
                      src={data?.user?.image as string | undefined}
                    />
                    <AvatarFallback>
                      {data?.user?.name?.split(" ")[0][0]}
                      {data?.user?.name?.split(" ")[1][0]}
                    </AvatarFallback>
                  </Avatar>

                  <div>
                    <h3 className="font-semibold">{data?.user?.name}</h3>
                    <span className="block text-xs text-muted-foreground">
                      {data?.user?.email}
                    </span>
                  </div>
                </div>
              </div>
            </>
          ) : (
            <>
              <div className="flex items-center justify-between pt-10">
                <h2 className="font-semibold">Olá. Faça seu login!</h2>
                <Button size="icon" onClick={handleSignInClick}>
                  <LogInIcon />
                </Button>
              </div>
            </>
          )}

          <div className="py-6">
            <Separator />
          </div>

          <div className="space-y-2">
            <Button
              variant="ghost"
              className="w-full justify-start space-x-3 rounded-full text-sm font-normal"
            >
              <HomeIcon size={16} />
              <span className="block">Início</span>
            </Button>

            {data?.user && (
              <>
                <Button
                  variant="ghost"
                  className="w-full justify-start space-x-3 rounded-full text-sm font-normal"
                  asChild
                >
                  <Link href="/my-orders">
                    <ScrollTextIcon size={16} />
                    <span className="block">Meus Pedidos</span>
                  </Link>
                </Button>

                <Button
                  variant="ghost"
                  className="w-full justify-start space-x-3 rounded-full text-sm font-normal"
                  asChild
                >
                  <Link href="/my-favorite-restaurants">
                    <HeartIcon size={16} />
                    <span className="block">Restaurantes Favoritos</span>
                  </Link>
                </Button>
              </>
            )}
          </div>

          <div className="py-6">
            <Separator />
          </div>

          {data?.user && (
            <Button
              variant="ghost"
              className="w-full justify-start space-x-3 rounded-full text-sm font-normal"
              onClick={handleSignOutClick}
            >
              <LogOutIcon size={16} />
              <span className="block">Sair da conta</span>
            </Button>
          )}
        </SheetContent>
      </Sheet>
    </div>
  );
};

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 (
    <Link
      className={cn("w-[150px] min-w-[150px]", className)}
      href={`/products/${product.id}`}
    >
      <div className="w-full space-y-2">
        <div className="relative aspect-square w-full">
          <Image
            src={product.imageUrl}
            alt={product.name}
            fill
            sizes="100%"
            className="rounded-lg object-cover shadow-md"
          />

          {product.discountPercentage && (
            <div className="absolute left-2 top-2 flex items-center gap-[2px] rounded-full bg-primary px-2 py-[2px] text-white">
              <ArrowDownIcon size={12} />
              <span className="text-xs font-semibold">
                {product.discountPercentage}%
              </span>
            </div>
          )}
        </div>

        <div>
          <h2 className="truncate text-sm">{product.name}</h2>
          <div className="flex items-center gap-1">
            <h3 className="font-semibold">
              {formatCurrency(calculateProductTotalPrice(product))}
            </h3>
            {product.discountPercentage > 0 && (
              <span className="text-xs text-muted-foreground line-through">
                {formatCurrency(Number(product.price))}
              </span>
            )}
          </div>

          <span className="block text-xs text-muted-foreground">
            {product.restaurant.name}
          </span>
        </div>
      </div>
    </Link>
  );
};

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 (
    <div className="flex gap-4 overflow-x-scroll px-5 [&::-webkit-scrollbar]:hidden">
      {products.map((product) => (
        <ProductItem key={product.id} product={product} />
      ))}
    </div>
  );
};

export default ProductList;


================================================
FILE: app/_components/promo-banner.tsx
================================================
import Image, { ImageProps } from "next/image";

const PromoBanner = (props: ImageProps) => {
  return (
    <Image
      height={0}
      width={0}
      className="h-auto w-full object-contain"
      sizes="100%"
      quality={100}
      {...props}
    />
  );
};

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 (
    <div className={cn("min-w-[266px] max-w-[266px]", className)}>
      <div className="w-full space-y-3">
        {/* IMAGEM */}
        <div className="relative h-[136px] w-full">
          <Link href={`/restaurants/${restaurant.id}`}>
            <Image
              src={restaurant.imageUrl}
              fill
              sizes="100%"
              className="rounded-lg object-cover"
              alt={restaurant.name}
            />
          </Link>

          <div className="absolute left-2 top-2 flex items-center gap-[2px] rounded-full bg-primary bg-white px-2 py-[2px]">
            <StarIcon size={12} className="fill-yellow-400 text-yellow-400" />
            <span className="text-xs font-semibold">5.0</span>
          </div>

          {data?.user.id && (
            <Button
              size="icon"
              className={`absolute right-2 top-2 h-7 w-7 rounded-full bg-gray-700 ${isFavorite && "bg-primary hover:bg-gray-700"}`}
              onClick={handleFavoriteClick}
            >
              <HeartIcon size={16} className="fill-white" />
            </Button>
          )}
        </div>
        {/* TEXTO */}
        <div>
          <h3 className="text-sm font-semibold">{restaurant.name}</h3>
          {/* INFORMAÇÕES DA ENTREGA */}
          <div className="flex gap-3">
            {/* CUSTO DE ENTREGA */}
            <div className="flex items-center gap-1">
              <BikeIcon className="text-primary" size={14} />
              <span className="text-xs text-muted-foreground">
                {Number(restaurant.deliveryFee) === 0
                  ? "Entrega grátis"
                  : formatCurrency(Number(restaurant.deliveryFee))}
              </span>
            </div>
            {/* TEMPO DE ENTREGA */}
            <div className="flex items-center gap-1">
              <TimerIcon className="text-primary" size={14} />
              <span className="text-xs text-muted-foreground">
                {restaurant.deliveryTimeMinutes} min
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

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 (
    <div className="flex gap-4 overflow-x-scroll px-5 [&::-webkit-scrollbar]:hidden">
      {restaurants.map((restaurant) => (
        <RestaurantItem
          key={restaurant.id}
          restaurant={restaurant}
          userFavoriteRestaurants={userFavoriteRestaurants}
        />
      ))}
    </div>
  );
};

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<HTMLInputElement>) => {
    setSearch(e.target.value);
  };

  const handleSearchSubmit: FormEventHandler<HTMLFormElement> = (e) => {
    e.preventDefault();

    if (!search) {
      return;
    }

    router.push(`/restaurants?search=${search}`);
  };

  return (
    <form className="flex gap-2" onSubmit={handleSearchSubmit}>
      <Input
        placeholder="Buscar restaurantes"
        className="border-none"
        onChange={handleChange}
        value={search}
      />
      <Button size="icon" type="submit">
        <SearchIcon size={20} />
      </Button>
    </form>
  );
};

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<typeof AlertDialogPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Overlay
    className={cn(
      "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
      className,
    )}
    {...props}
    ref={ref}
  />
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;

const AlertDialogContent = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
  <AlertDialogPortal>
    <AlertDialogOverlay />
    <AlertDialogPrimitive.Content
      ref={ref}
      className={cn(
        "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
        className,
      )}
      {...props}
    />
  </AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;

const AlertDialogHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col space-y-2 text-center sm:text-left",
      className,
    )}
    {...props}
  />
);
AlertDialogHeader.displayName = "AlertDialogHeader";

const AlertDialogFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
      className,
    )}
    {...props}
  />
);
AlertDialogFooter.displayName = "AlertDialogFooter";

const AlertDialogTitle = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Title
    ref={ref}
    className={cn("text-lg font-semibold", className)}
    {...props}
  />
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;

const AlertDialogDescription = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Description
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
));
AlertDialogDescription.displayName =
  AlertDialogPrimitive.Description.displayName;

const AlertDialogAction = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Action>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Action
    ref={ref}
    className={cn(buttonVariants(), className)}
    {...props}
  />
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;

const AlertDialogCancel = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Cancel
    ref={ref}
    className={cn(
      buttonVariants({ variant: "outline" }),
      "mt-2 sm:mt-0",
      className,
    )}
    {...props}
  />
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;

export {
  AlertDialog,
  AlertDialogPortal,
  AlertDialogOverlay,
  AlertDialogTrigger,
  AlertDialogContent,
  AlertDialogHeader,
  AlertDialogFooter,
  AlertDialogTitle,
  AlertDialogDescription,
  AlertDialogAction,
  AlertDialogCancel,
};


================================================
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<typeof AvatarPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Root
    ref={ref}
    className={cn(
      "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
      className,
    )}
    {...props}
  />
));
Avatar.displayName = AvatarPrimitive.Root.displayName;

const AvatarImage = React.forwardRef<
  React.ElementRef<typeof AvatarPrimitive.Image>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Image
    ref={ref}
    className={cn("aspect-square h-full w-full", className)}
    {...props}
  />
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;

const AvatarFallback = React.forwardRef<
  React.ElementRef<typeof AvatarPrimitive.Fallback>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Fallback
    ref={ref}
    className={cn(
      "flex h-full w-full items-center justify-center rounded-full bg-muted",
      className,
    )}
    {...props}
  />
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;

export { Avatar, AvatarImage, AvatarFallback };


================================================
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<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button";
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    );
  },
);
Button.displayName = "Button";

export { Button, buttonVariants };


================================================
FILE: app/_components/ui/card.tsx
================================================
import * as React from "react";

import { cn } from "@/app/_lib/utils";

const Card = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "rounded-lg border bg-card text-card-foreground shadow-sm",
      className
    )}
    {...props}
  />
));
Card.displayName = "Card";

const CardHeader = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex flex-col space-y-1.5 p-6", className)}
    {...props}
  />
));
CardHeader.displayName = "CardHeader";

const CardTitle = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
  <h3
    ref={ref}
    className={cn(
      "text-2xl font-semibold leading-none tracking-tight",
      className
    )}
    {...props}
  />
));
CardTitle.displayName = "CardTitle";

const CardDescription = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
  <p
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
));
CardDescription.displayName = "CardDescription";

const CardContent = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";

const CardFooter = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex items-center p-6 pt-0", className)}
    {...props}
  />
));
CardFooter.displayName = "CardFooter";

export {
  Card,
  CardHeader,
  CardFooter,
  CardTitle,
  CardDescription,
  CardContent,
};


================================================
FILE: app/_components/ui/input.tsx
================================================
import * as React from "react";

import { cn } from "@/app/_lib/utils";

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, ...props }, ref) => {
    return (
      <input
        type={type}
        className={cn(
          "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
          className,
        )}
        ref={ref}
        {...props}
      />
    );
  },
);
Input.displayName = "Input";

export { Input };


================================================
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<typeof SeparatorPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
  (
    { className, orientation = "horizontal", decorative = true, ...props },
    ref,
  ) => (
    <SeparatorPrimitive.Root
      ref={ref}
      decorative={decorative}
      orientation={orientation}
      className={cn(
        "shrink-0 bg-[#EEEEEE]",
        orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
        className,
      )}
      {...props}
    />
  ),
);
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<typeof SheetPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <SheetPrimitive.Overlay
    className={cn(
      "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
      className,
    )}
    {...props}
    ref={ref}
  />
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;

const sheetVariants = cva(
  "fixed z-50 gap-4 bg-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<typeof SheetPrimitive.Content>,
    VariantProps<typeof sheetVariants> {}

const SheetContent = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Content>,
  SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
  <SheetPortal>
    <SheetOverlay />
    <SheetPrimitive.Content
      ref={ref}
      className={cn(sheetVariants({ side }), className)}
      {...props}
    >
      {children}
      <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
        <X className="h-4 w-4" />
        <span className="sr-only">Close</span>
      </SheetPrimitive.Close>
    </SheetPrimitive.Content>
  </SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;

const SheetHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col space-y-2 text-center sm:text-left",
      className,
    )}
    {...props}
  />
);
SheetHeader.displayName = "SheetHeader";

const SheetFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
      className,
    )}
    {...props}
  />
);
SheetFooter.displayName = "SheetFooter";

const SheetTitle = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
  <SheetPrimitive.Title
    ref={ref}
    className={cn("text-lg font-semibold text-foreground", className)}
    {...props}
  />
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;

const SheetDescription = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
  <SheetPrimitive.Description
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;

export {
  Sheet,
  SheetPortal,
  SheetOverlay,
  SheetTrigger,
  SheetClose,
  SheetContent,
  SheetHeader,
  SheetFooter,
  SheetTitle,
  SheetDescription,
};


================================================
FILE: app/_components/ui/sonner.tsx
================================================
"use client";

import { useTheme } from "next-themes";
import { Toaster as Sonner } from "sonner";

type ToasterProps = React.ComponentProps<typeof Sonner>;

const Toaster = ({ ...props }: ToasterProps) => {
  const { theme = "system" } = useTheme();

  return (
    <Sonner
      theme={theme as ToasterProps["theme"]}
      className="toaster group"
      toastOptions={{
        classNames: {
          toast:
            "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
          description: "group-[.toast]:text-muted-foreground",
          actionButton:
            "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
          cancelButton:
            "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
        },
      }}
      {...props}
    />
  );
};

export { Toaster };


================================================
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<ICartContext>({
  products: [],
  subtotalPrice: 0,
  totalPrice: 0,
  totalDiscounts: 0,
  totalQuantity: 0,
  addProductToCart: () => {},
  decreaseProductQuantity: () => {},
  increaseProductQuantity: () => {},
  removeProductFromCart: () => {},
  clearCart: () => {},
});

export const CartProvider = ({ children }: { children: ReactNode }) => {
  const [products, setProducts] = useState<CartProduct[]>([]);

  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 (
    <CartContext.Provider
      value={{
        products,
        subtotalPrice,
        totalPrice,
        totalDiscounts,
        totalQuantity,
        clearCart,
        addProductToCart,
        decreaseProductQuantity,
        increaseProductQuantity,
        removeProductFromCart,
      }}
    >
      {children}
    </CartContext.Provider>
  );
};


================================================
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 <SessionProvider>{children}</SessionProvider>;
};

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 (
    <>
      <Header />
      <div className="px-5 py-6">
        <h2 className="mb-6 text-lg font-semibold">{category.name}</h2>
        <div className="grid grid-cols-2 gap-6">
          {category.products.map((product) => (
            <ProductItem
              key={product.id}
              product={product}
              className="min-w-full"
            />
          ))}
        </div>
      </div>
    </>
  );
};

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 (
    <html lang="en">
      <body className={poppins.className}>
        <AuthProvider>
          <CartProvider>{children}</CartProvider>
        </AuthProvider>

        <Toaster />
      </body>
    </html>
  );
}


================================================
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 (
    <>
      <Header />
      <div className="px-5 py-6">
        <h2 className="mb-6 text-lg font-semibold">Restaurantes Favoritos</h2>
        <div className="flex w-full flex-col gap-6">
          {userFavoriteRestaurants.length > 0 ? (
            userFavoriteRestaurants.map(({ restaurant }) => (
              <RestaurantItem
                key={restaurant.id}
                restaurant={restaurant}
                className="min-w-full max-w-full"
                userFavoriteRestaurants={userFavoriteRestaurants}
              />
            ))
          ) : (
            <h3 className="font-medium">
              Você ainda não marcou nenhum restaurante como favorito.
            </h3>
          )}
        </div>
      </div>
    </>
  );
};

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 (
    <Card>
      <CardContent className="p-5">
        <div
          className={`w-fit rounded-full bg-[#EEEEEE] px-2 py-1 text-muted-foreground ${order.status !== "COMPLETED" && "bg-green-500 text-white"}`}
        >
          <span className="block text-xs font-semibold">
            {getOrderStatusLabel(order.status)}
          </span>
        </div>

        <div className="flex items-center justify-between pt-3">
          <div className="flex items-center gap-2">
            <Avatar className="h-6 w-6">
              <AvatarImage src={order.restaurant.imageUrl} />
            </Avatar>

            <span className="text-sm font-semibold">
              {order.restaurant.name}
            </span>
          </div>

          <Button
            variant="link"
            size="icon"
            className="h-5 w-5 text-black"
            asChild
          >
            <Link href={`/restaurants/${order.restaurantId}`}>
              <ChevronRightIcon />
            </Link>
          </Button>
        </div>

        <div className="py-3">
          <Separator />
        </div>

        <div className="space-y-2">
          {order.products.map((product) => (
            <div key={product.id} className="flex items-center gap-2">
              <div className="flex h-5 w-5 items-center justify-center rounded-full bg-muted-foreground">
                <span className="block text-xs text-white">
                  {product.quantity}
                </span>
              </div>
              <span className="block text-xs text-muted-foreground">
                {product.product.name}
              </span>
            </div>
          ))}
        </div>

        <div className="py-3">
          <Separator />
        </div>

        <div className="flex items-center justify-between">
          <p className="text-sm">{formatCurrency(Number(order.totalPrice))}</p>
          <Button
            variant="ghost"
            size="sm"
            className="text-xs text-primary"
            disabled={order.status !== "COMPLETED"}
            onClick={handleRedoOrderClick}
          >
            Refazer pedido
          </Button>
        </div>
      </CardContent>
    </Card>
  );
};

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 (
    <>
      <Header />

      <div className="px-5 py-6">
        <h2 className="pb-6 text-lg font-semibold">Meus Pedidos</h2>

        <div className="space-y-4">
          {orders.map((order) => (
            <OrderItem key={order.id} order={order} />
          ))}
        </div>
      </div>
    </>
  );
};

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 (
    <>
      <Header />
      <div className="px-5 pt-6">
        <Search />
      </div>

      <div className="px-5 pt-6">
        <CategoryList />
      </div>

      <div className="px-5 pt-6">
        <Link href={`/categories/${pizzasCategory?.id}/products`}>
          <PromoBanner
            src="/promo-banner-01.png"
            alt="Até 30% de desconto em pizzas!"
          />
        </Link>
      </div>

      <div className="space-y-4 pt-6">
        <div className="flex items-center justify-between px-5">
          <h2 className="font-semibold">Pedidos Recomendados</h2>

          <Button
            variant="ghost"
            className="h-fit p-0 text-primary hover:bg-transparent"
            asChild
          >
            <Link href="/products/recommended">
              Ver todos
              <ChevronRightIcon size={16} />
            </Link>
          </Button>
        </div>
        <ProductList products={products} />
      </div>

      <div className="px-5 pt-6">
        <Link href={`/categories/${burguersCategory?.id}/products`}>
          <PromoBanner
            src="/promo-banner-02.png"
            alt="A partir de R$17,90 em lanches"
          />
        </Link>
      </div>

      <div className="space-y-4 py-6">
        <div className="flex items-center justify-between px-5">
          <h2 className="font-semibold">Restaurantes Recomendados</h2>

          <Button
            variant="ghost"
            className="h-fit p-0 text-primary hover:bg-transparent"
            asChild
          >
            <Link href="/restaurants/recommended">
              Ver todos
              <ChevronRightIcon size={16} />
            </Link>
          </Button>
        </div>
        <RestaurantList />
      </div>
    </>
  );
};

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 (
    <>
      <div className="relative z-50 mt-[-1.5rem] rounded-tl-3xl rounded-tr-3xl bg-white py-5">
        {/* RESTAURANTE */}
        <div className="flex items-center gap-[0.375rem] px-5">
          <div className="relative h-6 w-6">
            <Image
              src={product.restaurant.imageUrl}
              alt={product.restaurant.name}
              fill
              sizes="100%"
              className="rounded-full object-cover"
            />
          </div>
          <span className="text-xs text-muted-foreground">
            {product.restaurant.name}
          </span>
        </div>

        {/* NOME DO PRODUTO */}
        <h1 className="mb-2 mt-1 px-5 text-xl font-semibold">{product.name}</h1>

        {/* PREÇO DO PRODUTO E QUANTIDADE */}
        <div className="flex justify-between px-5">
          {/* PREÇO COM DESCONTO */}
          <div>
            <div className="flex items-center gap-2">
              <h2 className="text-xl font-semibold">
                {formatCurrency(calculateProductTotalPrice(product))}
              </h2>
              {product.discountPercentage > 0 && (
                <DiscountBadge product={product} />
              )}
            </div>

            {/* PREÇO ORIGINAL */}
            {product.discountPercentage > 0 && (
              <p className="text-sm text-muted-foreground">
                De: {formatCurrency(Number(product.price))}
              </p>
            )}
          </div>

          {/* QUANTIDADE */}
          <div className="flex items-center gap-3 text-center">
            <Button
              size="icon"
              variant="ghost"
              className="border border-solid border-muted-foreground"
              onClick={handleDecreaseQuantityClick}
            >
              <ChevronLeftIcon />
            </Button>
            <span className="w-4">{quantity}</span>
            <Button size="icon" onClick={handleIncreaseQuantityClick}>
              <ChevronRightIcon />
            </Button>
          </div>
        </div>

        <div className="px-5">
          <DeliveryInfo restaurant={product.restaurant} />
        </div>

        <div className="mt-6 space-y-3 px-5">
          <h3 className="font-semibold">Sobre</h3>
          <p className="text-sm text-muted-foreground">{product.description}</p>
        </div>

        <div className="mt-6 space-y-3">
          <h3 className="px-5 font-semibold">Sucos</h3>
          <ProductList products={complementaryProducts} />
        </div>

        <div className="mt-6 px-5">
          <Button
            className="w-full font-semibold"
            onClick={handleAddToCartClick}
          >
            Adicionar à sacola
          </Button>
        </div>
      </div>

      <Sheet open={isCartOpen} onOpenChange={setIsCartOpen}>
        <SheetContent className="w-[90vw]">
          <SheetHeader>
            <SheetTitle className="text-left">Sacola</SheetTitle>
          </SheetHeader>

          <Cart setIsOpen={setIsCartOpen} />
        </SheetContent>
      </Sheet>

      <AlertDialog
        open={isConfirmationDialogOpen}
        onOpenChange={setIsConfirmationDialogOpen}
      >
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>
              Você só pode adicionar itens de um restaurante por vez
            </AlertDialogTitle>
            <AlertDialogDescription>
              Deseja mesmo adicionar esse produto? Isso limpará sua sacola
              atual.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancelar</AlertDialogCancel>
            <AlertDialogAction onClick={() => addToCart({ emptyCart: true })}>
              Esvaziar sacola e adicionar
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  );
};

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<Product, "name" | "imageUrl">;
}

const ProductImage = ({ product }: ProductImageProps) => {
  const router = useRouter();

  const handleBackClick = () => router.back();

  return (
    <div className="relative h-[360px] w-full">
      <Image
        src={product.imageUrl}
        alt={product.name}
        fill
        sizes="100%"
        className="object-cover"
      />

      <Button
        className="absolute left-4 top-4 rounded-full bg-white text-foreground hover:text-white"
        size="icon"
        onClick={handleBackClick}
      >
        <ChevronLeftIcon />
      </Button>
    </div>
  );
};

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 (
    <div>
      {/* IMAGEM */}
      <ProductImage product={product} />

      {/* TITULO E PREÇO */}
      <ProductDetails product={product} complementaryProducts={juices} />
    </div>
  );
};

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 (
    <>
      <Header />
      <div className="px-5 py-6">
        <h2 className="mb-6 text-lg font-semibold">Pedidos Recomendados</h2>
        <div className="grid grid-cols-2 gap-6">
          {products.map((product) => (
            <ProductItem
              key={product.id}
              product={product}
              className="min-w-full"
            />
          ))}
        </div>
      </div>
    </>
  );
};

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<Restaurant, "id">;
}

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 (
    <div className="fixed bottom-0 left-0 z-50 w-full border-t border-solid border-muted bg-white p-5 pt-3 shadow-md">
      <div className="flex items-center justify-between">
        {/* PREÇO */}
        <div>
          <span className="text-xs text-muted-foreground">
            Total sem entrega
          </span>
          <h3 className="font-semibold">
            {formatCurrency(totalPrice)}{" "}
            <span className="text-xs font-normal text-muted-foreground">
              {" "}
              / {totalQuantity} {totalQuantity > 1 ? "itens" : "item"}
            </span>
          </h3>
        </div>
        {/* BOTÃO */}

        <Button onClick={() => setIsCartOpen(true)}>Ver sacola</Button>

        <Sheet open={isCartOpen} onOpenChange={setIsCartOpen}>
          <SheetContent className="w-[90vw]">
            <SheetHeader>
              <SheetTitle className="text-left">Sacola</SheetTitle>
            </SheetHeader>

            <Cart setIsOpen={setIsCartOpen} />
          </SheetContent>
        </Sheet>
      </div>
    </div>
  );
};

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<Restaurant, "id" | "name" | "imageUrl">;
  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 (
    <div className="relative h-[250px] w-full">
      <Image
        src={restaurant.imageUrl}
        alt={restaurant.name}
        fill
        sizes="100%"
        className="object-cover"
      />

      <Button
        className="absolute left-4 top-4 rounded-full bg-white text-foreground hover:text-white"
        size="icon"
        onClick={handleBackClick}
      >
        <ChevronLeftIcon />
      </Button>

      <Button
        size="icon"
        className={`absolute right-4 top-4 rounded-full bg-gray-700 ${isFavorite && "bg-primary hover:bg-gray-700"}`}
        onClick={handleFavoriteClick}
      >
        <HeartIcon size={20} className="fill-white" />
      </Button>
    </div>
  );
};

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 (
    <div>
      <RestaurantImage
        restaurant={restaurant}
        userFavoriteRestaurants={userFavoriteRestaurants}
      />

      <div className="relative z-50 mt-[-1.5rem] flex items-center justify-between rounded-tl-3xl rounded-tr-3xl bg-white px-5 pt-5">
        {/* TITULO */}
        <div className="flex items-center gap-[0.375rem]">
          <div className="relative h-8 w-8">
            <Image
              src={restaurant.imageUrl}
              alt={restaurant.name}
              fill
              sizes="100%"
              className="rounded-full object-cover"
            />
          </div>
          <h1 className="text-xl font-semibold">{restaurant.name}</h1>
        </div>

        <div className="flex items-center gap-[3px] rounded-full bg-foreground px-2 py-[2px] text-white">
          <StarIcon size={12} className="fill-yellow-400 text-yellow-400" />
          <span className="text-xs font-semibold">5.0</span>
        </div>
      </div>

      <div className="px-5">
        <DeliveryInfo restaurant={restaurant} />
      </div>

      <div className="mt-3 flex gap-4 overflow-x-scroll px-5 [&::-webkit-scrollbar]:hidden">
        {restaurant.categories.map((category) => (
          <div
            key={category.id}
            className="min-w-[167px] rounded-lg bg-[#F4F4F4] text-center"
          >
            <span className="text-xs text-muted-foreground">
              {category.name}
            </span>
          </div>
        ))}
      </div>

      <div className="mt-6 space-y-4">
        {/* TODO: mostrar produtos mais pedidos quando implementarmos realização de pedido */}
        <h2 className="px-5  font-semibold">Mais Pedidos</h2>
        <ProductList products={restaurant.products} />
      </div>

      {restaurant.categories.map((category) => (
        <div className="mt-6 space-y-4" key={category.id}>
          {/* TODO: mostrar produtos mais pedidos quando implementarmos realização de pedido */}
          <h2 className="px-5  font-semibold">{category.name}</h2>
          <ProductList products={category.products} />
        </div>
      ))}

      <CartBanner restaurant={restaurant} />
    </div>
  );
};

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<Restaurant[]>([]);

  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 (
    <>
      <Header />
      <div className="px-5 py-6">
        <h2 className="mb-6 text-lg font-semibold">Restaurantes Encontrados</h2>
        <div className="flex w-full flex-col gap-6">
          {restaurants.map((restaurant) => (
            <RestaurantItem
              key={restaurant.id}
              restaurant={restaurant}
              className="min-w-full max-w-full"
              userFavoriteRestaurants={userFavoriteRestaurants}
            />
          ))}
        </div>
      </div>
    </>
  );
};

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 (
    <Suspense>
      <Restaurants userFavoriteRestaurants={userFavoriteRestaurants} />
    </Suspense>
  );
};

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 (
    <>
      <Header />
      <div className="px-5 py-6">
        <h2 className="mb-6 text-lg font-semibold">
          Restaurantes Recomendados
        </h2>
        <div className="flex w-full flex-col gap-6">
          {restaurants.map((restaurant) => (
            <RestaurantItem
              key={restaurant.id}
              restaurant={restaurant}
              className="min-w-full max-w-full"
              userFavoriteRestaurants={userFavoriteRestaurants}
            />
          ))}
        </div>
      </div>
    </>
  );
};

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"]
}
Download .txt
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
Download .txt
SYMBOL INDEX (48 symbols across 32 files)

FILE: app/_components/cart-item.tsx
  type CartItemProps (line 8) | interface CartItemProps {

FILE: app/_components/cart.tsx
  type CartProps (line 25) | interface CartProps {

FILE: app/_components/category-item.tsx
  type CategoryItemProps (line 5) | interface CategoryItemProps {

FILE: app/_components/delivery-info.tsx
  type DeliveryInfoProps (line 6) | interface DeliveryInfoProps {

FILE: app/_components/discount-badge.tsx
  type DiscountBadgeProps (line 4) | interface DiscountBadgeProps {

FILE: app/_components/product-item.tsx
  type ProductItemProps (line 10) | interface ProductItemProps {

FILE: app/_components/product-list.tsx
  type ProductListProps (line 4) | interface ProductListProps {

FILE: app/_components/restaurant-item.tsx
  type RestaurantItemProps (line 14) | interface RestaurantItemProps {

FILE: app/_components/ui/button.tsx
  type ButtonProps (line 36) | interface ButtonProps

FILE: app/_components/ui/input.tsx
  type InputProps (line 5) | interface InputProps

FILE: app/_components/ui/sheet.tsx
  type SheetContentProps (line 52) | interface SheetContentProps

FILE: app/_components/ui/sonner.tsx
  type ToasterProps (line 6) | type ToasterProps = React.ComponentProps<typeof Sonner>;

FILE: app/_context/cart.tsx
  type CartProduct (line 8) | interface CartProduct
  type ICartContext (line 23) | interface ICartContext {

FILE: app/_hooks/use-toggle-favorite-restaurant.ts
  type UseToggleFavoriteRestaurantProps (line 6) | interface UseToggleFavoriteRestaurantProps {

FILE: app/_lib/auth.ts
  method session (line 16) | async session({ session, user }) {

FILE: app/_lib/utils.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {

FILE: app/categories/[id]/products/page.tsx
  type CategoriesPageProps (line 6) | interface CategoriesPageProps {

FILE: app/layout.tsx
  function RootLayout (line 18) | function RootLayout({

FILE: app/my-orders/_components/order-item.tsx
  type OrderItemProps (line 15) | interface OrderItemProps {

FILE: app/products/[id]/_components/product-details.tsx
  type ProductDetailsProps (line 34) | interface ProductDetailsProps {

FILE: app/products/[id]/_components/product-image.tsx
  type ProductImageProps (line 9) | interface ProductImageProps {

FILE: app/products/[id]/page.tsx
  type ProductPageProps (line 6) | interface ProductPageProps {

FILE: app/restaurants/[id]/_components/cart-banner.tsx
  type CartBannerProps (line 16) | interface CartBannerProps {

FILE: app/restaurants/[id]/_components/restaurant-image.tsx
  type RestaurantImageProps (line 12) | interface RestaurantImageProps {

FILE: app/restaurants/[id]/page.tsx
  type RestaurantPageProps (line 12) | interface RestaurantPageProps {

FILE: app/restaurants/_components/restaurants.tsx
  type RestaurantProps (line 10) | interface RestaurantProps {

FILE: next-auth.d.ts
  type Session (line 5) | interface Session {

FILE: prisma/migrations/20240415210903_init_database/migration.sql
  type "Restaurant" (line 2) | CREATE TABLE "Restaurant" (
  type "Category" (line 13) | CREATE TABLE "Category" (
  type "Product" (line 22) | CREATE TABLE "Product" (
  type "_CategoryToRestaurant" (line 36) | CREATE TABLE "_CategoryToRestaurant" (
  type "_CategoryToRestaurant" (line 42) | CREATE UNIQUE INDEX "_CategoryToRestaurant_AB_unique" ON "_CategoryToRes...
  type "_CategoryToRestaurant" (line 45) | CREATE INDEX "_CategoryToRestaurant_B_index" ON "_CategoryToRestaurant"(...

FILE: prisma/migrations/20240502232158_add_auth_tables/migration.sql
  type "accounts" (line 2) | CREATE TABLE "accounts" (
  type "sessions" (line 20) | CREATE TABLE "sessions" (
  type "users" (line 30) | CREATE TABLE "users" (
  type "verificationtokens" (line 41) | CREATE TABLE "verificationtokens" (
  type "accounts" (line 48) | CREATE UNIQUE INDEX "accounts_provider_provider_account_id_key" ON "acco...
  type "sessions" (line 51) | CREATE UNIQUE INDEX "sessions_session_token_key" ON "sessions"("session_...
  type "users" (line 54) | CREATE UNIQUE INDEX "users_email_key" ON "users"("email")
  type "verificationtokens" (line 57) | CREATE UNIQUE INDEX "verificationtokens_identifier_token_key" ON "verifi...

FILE: prisma/migrations/20240503000412_add_order_table/migration.sql
  type "Order" (line 5) | CREATE TABLE "Order" (
  type "_OrderToProduct" (line 21) | CREATE TABLE "_OrderToProduct" (
  type "_OrderToProduct" (line 27) | CREATE UNIQUE INDEX "_OrderToProduct_AB_unique" ON "_OrderToProduct"("A"...
  type "_OrderToProduct" (line 30) | CREATE INDEX "_OrderToProduct_B_index" ON "_OrderToProduct"("B")

FILE: prisma/migrations/20240503012547_add_order_product_table/migration.sql
  type "OrderProduct" (line 20) | CREATE TABLE "OrderProduct" (

FILE: prisma/migrations/20240503233901_add_user_restaurant_favorites_table/migration.sql
  type "UserFavoriteRestaurant" (line 2) | CREATE TABLE "UserFavoriteRestaurant" (
Condensed preview — 76 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (139K chars).
[
  {
    "path": ".eslintrc.json",
    "chars": 88,
    "preview": "{\n  \"extends\": \"next/core-web-vitals\",\n  \"rules\": {\n    \"no-unused-vars\": \"error\"\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "chars": 420,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": ".husky/commit-msg",
    "chars": 66,
    "preview": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\n.git/hooks/commit-msg $1"
  },
  {
    "path": ".husky/pre-commit",
    "chars": 16,
    "preview": "npx lint-staged\n"
  },
  {
    "path": ".lintstagedrc.json",
    "chars": 55,
    "preview": "{\n  \"*.ts?(x)\": [\"eslint --fix\", \"prettier --write\"]\n}\n"
  },
  {
    "path": ".prettierrc",
    "chars": 49,
    "preview": "{\n  \"plugins\": [\"prettier-plugin-tailwindcss\"]\n}\n"
  },
  {
    "path": "README.md",
    "chars": 1383,
    "preview": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js"
  },
  {
    "path": "app/_actions/order.ts",
    "chars": 279,
    "preview": "\"use server\";\n\nimport { Prisma } from \"@prisma/client\";\nimport { db } from \"../_lib/prisma\";\nimport { revalidatePath } f"
  },
  {
    "path": "app/_actions/restaurant.ts",
    "chars": 674,
    "preview": "\"use server\";\n\nimport { revalidatePath } from \"next/cache\";\nimport { db } from \"../_lib/prisma\";\n\nexport const toggleFav"
  },
  {
    "path": "app/_components/cart-item.tsx",
    "chars": 3000,
    "preview": "import Image from \"next/image\";\nimport { CartContext, CartProduct } from \"../_context/cart\";\nimport { calculateProductTo"
  },
  {
    "path": "app/_components/cart.tsx",
    "chars": 5658,
    "preview": "import { useContext, useState } from \"react\";\nimport { CartContext } from \"../_context/cart\";\nimport CartItem from \"./ca"
  },
  {
    "path": "app/_components/category-item.tsx",
    "chars": 639,
    "preview": "import { Category } from \"@prisma/client\";\nimport Image from \"next/image\";\nimport Link from \"next/link\";\n\ninterface Cate"
  },
  {
    "path": "app/_components/category-list.tsx",
    "chars": 387,
    "preview": "import { db } from \"../_lib/prisma\";\nimport CategoryItem from \"./category-item\";\n\nconst CategoryList = async () => {\n  c"
  },
  {
    "path": "app/_components/delivery-info.tsx",
    "chars": 1406,
    "preview": "import { BikeIcon, TimerIcon } from \"lucide-react\";\nimport { Card } from \"./ui/card\";\nimport { formatCurrency } from \".."
  },
  {
    "path": "app/_components/discount-badge.tsx",
    "chars": 525,
    "preview": "import { Product } from \"@prisma/client\";\nimport { ArrowDownIcon } from \"lucide-react\";\n\ninterface DiscountBadgeProps {\n"
  },
  {
    "path": "app/_components/header.tsx",
    "chars": 4403,
    "preview": "\"use client\";\n\nimport Image from \"next/image\";\nimport { Button } from \"./ui/button\";\nimport {\n  HeartIcon,\n  HomeIcon,\n "
  },
  {
    "path": "app/_components/product-item.tsx",
    "chars": 2045,
    "preview": "\"use client\";\n\nimport { Prisma } from \"@prisma/client\";\nimport Image from \"next/image\";\nimport { calculateProductTotalPr"
  },
  {
    "path": "app/_components/product-list.tsx",
    "chars": 572,
    "preview": "import { Prisma } from \"@prisma/client\";\nimport ProductItem from \"./product-item\";\n\ninterface ProductListProps {\n  produ"
  },
  {
    "path": "app/_components/promo-banner.tsx",
    "chars": 296,
    "preview": "import Image, { ImageProps } from \"next/image\";\n\nconst PromoBanner = (props: ImageProps) => {\n  return (\n    <Image\n    "
  },
  {
    "path": "app/_components/restaurant-item.tsx",
    "chars": 3401,
    "preview": "\"use client\";\n\nimport { Restaurant, UserFavoriteRestaurant } from \"@prisma/client\";\nimport { BikeIcon, HeartIcon, StarIc"
  },
  {
    "path": "app/_components/restaurant-list.tsx",
    "chars": 877,
    "preview": "import { getServerSession } from \"next-auth\";\nimport { db } from \"../_lib/prisma\";\nimport RestaurantItem from \"./restaur"
  },
  {
    "path": "app/_components/search.tsx",
    "chars": 988,
    "preview": "\"use client\";\n\nimport { SearchIcon } from \"lucide-react\";\nimport { Button } from \"./ui/button\";\nimport { Input } from \"."
  },
  {
    "path": "app/_components/ui/alert-dialog.tsx",
    "chars": 4474,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\n\nim"
  },
  {
    "path": "app/_components/ui/avatar.tsx",
    "chars": 1437,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\n\nimport { cn }"
  },
  {
    "path": "app/_components/ui/button.tsx",
    "chars": 1855,
    "preview": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"cla"
  },
  {
    "path": "app/_components/ui/card.tsx",
    "chars": 1910,
    "preview": "import * as React from \"react\";\n\nimport { cn } from \"@/app/_lib/utils\";\n\nconst Card = React.forwardRef<\n  HTMLDivElement"
  },
  {
    "path": "app/_components/ui/input.tsx",
    "chars": 837,
    "preview": "import * as React from \"react\";\n\nimport { cn } from \"@/app/_lib/utils\";\n\nexport interface InputProps\n  extends React.Inp"
  },
  {
    "path": "app/_components/ui/separator.tsx",
    "chars": 788,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\n\nimport "
  },
  {
    "path": "app/_components/ui/sheet.tsx",
    "chars": 4309,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { cva, t"
  },
  {
    "path": "app/_components/ui/sonner.tsx",
    "chars": 902,
    "preview": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport { Toaster as Sonner } from \"sonner\";\n\ntype ToasterProps = "
  },
  {
    "path": "app/_context/cart.tsx",
    "chars": 4273,
    "preview": "/* eslint-disable no-unused-vars */\n\"use client\";\n\nimport { Prisma } from \"@prisma/client\";\nimport { ReactNode, createCo"
  },
  {
    "path": "app/_helpers/price.ts",
    "chars": 503,
    "preview": "import { Product } from \"@prisma/client\";\n\nexport const calculateProductTotalPrice = (product: Product): number => {\n  i"
  },
  {
    "path": "app/_helpers/restaurant.ts",
    "chars": 254,
    "preview": "import { UserFavoriteRestaurant } from \"@prisma/client\";\n\nexport const isRestaurantFavorited = (\n  restaurantId: string,"
  },
  {
    "path": "app/_hooks/use-toggle-favorite-restaurant.ts",
    "chars": 1154,
    "preview": "import { toast } from \"sonner\";\nimport { toggleFavoriteRestaurant } from \"../_actions/restaurant\";\nimport { UserFavorite"
  },
  {
    "path": "app/_lib/auth.ts",
    "chars": 667,
    "preview": "import { PrismaAdapter } from \"@auth/prisma-adapter\";\nimport { AuthOptions } from \"next-auth\";\nimport { db } from \"./pri"
  },
  {
    "path": "app/_lib/prisma.ts",
    "chars": 390,
    "preview": "/* eslint-disable no-unused-vars */\nimport { PrismaClient } from \"@prisma/client\";\n\ndeclare global {\n  var cachedPrisma:"
  },
  {
    "path": "app/_lib/utils.ts",
    "chars": 166,
    "preview": "import { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: Cla"
  },
  {
    "path": "app/_providers/auth.tsx",
    "chars": 257,
    "preview": "\"use client\";\n\nimport { SessionProvider } from \"next-auth/react\";\nimport { ReactNode } from \"react\";\n\nconst AuthProvider"
  },
  {
    "path": "app/api/auth/[...nextauth]/route.ts",
    "chars": 166,
    "preview": "import NextAuth from \"next-auth\";\nimport { authOptions } from \"@/app/_lib/auth\";\n\nconst handler = NextAuth(authOptions);"
  },
  {
    "path": "app/categories/[id]/products/page.tsx",
    "chars": 1115,
    "preview": "import Header from \"@/app/_components/header\";\nimport ProductItem from \"@/app/_components/product-item\";\nimport { db } f"
  },
  {
    "path": "app/globals.css",
    "chars": 1357,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n  --background: 0 0% 96%;\n  --foreground: 0 0% 20%;\n"
  },
  {
    "path": "app/layout.tsx",
    "chars": 795,
    "preview": "import type { Metadata } from \"next\";\nimport { Poppins } from \"next/font/google\";\nimport \"./globals.css\";\nimport { CartP"
  },
  {
    "path": "app/my-favorite-restaurants/page.tsx",
    "chars": 1412,
    "preview": "import { getServerSession } from \"next-auth\";\nimport { db } from \"../_lib/prisma\";\nimport { authOptions } from \"../_lib/"
  },
  {
    "path": "app/my-orders/_components/order-item.tsx",
    "chars": 3835,
    "preview": "\"use client\";\n\nimport { Avatar, AvatarImage } from \"@/app/_components/ui/avatar\";\nimport { Button } from \"@/app/_compone"
  },
  {
    "path": "app/my-orders/page.tsx",
    "chars": 995,
    "preview": "import { getServerSession } from \"next-auth\";\nimport { db } from \"../_lib/prisma\";\nimport { authOptions } from \"../_lib/"
  },
  {
    "path": "app/page.tsx",
    "chars": 3070,
    "preview": "import CategoryList from \"./_components/category-list\";\nimport Header from \"./_components/header\";\nimport Search from \"."
  },
  {
    "path": "app/products/[id]/_components/product-details.tsx",
    "chars": 6325,
    "preview": "\"use client\";\n\nimport Cart from \"@/app/_components/cart\";\nimport DeliveryInfo from \"@/app/_components/delivery-info\";\nim"
  },
  {
    "path": "app/products/[id]/_components/product-image.tsx",
    "chars": 928,
    "preview": "\"use client\";\n\nimport { Button } from \"@/app/_components/ui/button\";\nimport { Product } from \"@prisma/client\";\nimport { "
  },
  {
    "path": "app/products/[id]/page.tsx",
    "chars": 981,
    "preview": "import { db } from \"@/app/_lib/prisma\";\nimport { notFound } from \"next/navigation\";\nimport ProductImage from \"./_compone"
  },
  {
    "path": "app/products/recommended/page.tsx",
    "chars": 950,
    "preview": "import Header from \"@/app/_components/header\";\nimport ProductItem from \"@/app/_components/product-item\";\nimport { db } f"
  },
  {
    "path": "app/restaurants/[id]/_components/cart-banner.tsx",
    "chars": 1944,
    "preview": "\"use client\";\n\nimport Cart from \"@/app/_components/cart\";\nimport { Button } from \"@/app/_components/ui/button\";\nimport {"
  },
  {
    "path": "app/restaurants/[id]/_components/restaurant-image.tsx",
    "chars": 1855,
    "preview": "\"use client\";\n\nimport { Button } from \"@/app/_components/ui/button\";\nimport { isRestaurantFavorited } from \"@/app/_helpe"
  },
  {
    "path": "app/restaurants/[id]/page.tsx",
    "chars": 3759,
    "preview": "import { db } from \"@/app/_lib/prisma\";\nimport { notFound } from \"next/navigation\";\nimport RestaurantImage from \"./_comp"
  },
  {
    "path": "app/restaurants/_actions/search.ts",
    "chars": 303,
    "preview": "\"use server\";\n\nimport { db } from \"@/app/_lib/prisma\";\n\nexport const searchForRestaurants = async (search: string) => {\n"
  },
  {
    "path": "app/restaurants/_components/restaurants.tsx",
    "chars": 1542,
    "preview": "\"use client\";\n\nimport { Restaurant, UserFavoriteRestaurant } from \"@prisma/client\";\nimport { notFound, useSearchParams }"
  },
  {
    "path": "app/restaurants/page.tsx",
    "chars": 645,
    "preview": "import { Suspense } from \"react\";\nimport Restaurants from \"./_components/restaurants\";\nimport { getServerSession } from "
  },
  {
    "path": "app/restaurants/recommended/page.tsx",
    "chars": 1177,
    "preview": "import Header from \"@/app/_components/header\";\nimport RestaurantItem from \"@/app/_components/restaurant-item\";\nimport { "
  },
  {
    "path": "components.json",
    "chars": 352,
    "preview": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n"
  },
  {
    "path": "docker-compose.yml",
    "chars": 305,
    "preview": "version: \"3\"\n\nservices:\n  postgres:\n    image: postgres:latest\n    container_name: fsw-foods-postgres\n    environment:\n "
  },
  {
    "path": "next-auth.d.ts",
    "chars": 201,
    "preview": "/* eslint-disable no-unused-vars */\nimport { DefaultSession } from \"next-auth\";\n\ndeclare module \"next-auth\" {\n  interfac"
  },
  {
    "path": "next.config.mjs",
    "chars": 157,
    "preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  images: {\n    remotePatterns: [{ hostname: \"utfs.io\" }],"
  },
  {
    "path": "package.json",
    "chars": 1361,
    "preview": "{\n  \"name\": \"fsw-foods\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"ne"
  },
  {
    "path": "postcss.config.mjs",
    "chars": 135,
    "preview": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n  plugins: {\n    tailwindcss: {},\n  },\n};\n\nexport d"
  },
  {
    "path": "prisma/migrations/20240415210903_init_database/migration.sql",
    "chars": 1830,
    "preview": "-- CreateTable\nCREATE TABLE \"Restaurant\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"imageUrl\" TEXT NOT NUL"
  },
  {
    "path": "prisma/migrations/20240501004806_add_created_at/migration.sql",
    "chars": 226,
    "preview": "-- AlterTable\nALTER TABLE \"Category\" ADD COLUMN     \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;\n\n-- Alt"
  },
  {
    "path": "prisma/migrations/20240502232158_add_auth_tables/migration.sql",
    "chars": 1743,
    "preview": "-- CreateTable\nCREATE TABLE \"accounts\" (\n    \"id\" TEXT NOT NULL,\n    \"user_id\" TEXT NOT NULL,\n    \"type\" TEXT NOT NULL,\n"
  },
  {
    "path": "prisma/migrations/20240503000412_add_order_table/migration.sql",
    "chars": 1557,
    "preview": "-- CreateEnum\nCREATE TYPE \"OrderStatus\" AS ENUM ('CONFIRMED', 'CANCELED', 'PREPARING', 'DELIVERING', 'COMPLETED');\n\n-- C"
  },
  {
    "path": "prisma/migrations/20240503012547_add_order_product_table/migration.sql",
    "chars": 1178,
    "preview": "/*\n  Warnings:\n\n  - You are about to drop the `_OrderToProduct` table. If the table is not empty, all the data it contai"
  },
  {
    "path": "prisma/migrations/20240503012822_add_order_product_table/migration.sql",
    "chars": 270,
    "preview": "/*\n  Warnings:\n\n  - You are about to drop the column `productId` on the `Order` table. All the data in the column will b"
  },
  {
    "path": "prisma/migrations/20240503233901_add_user_restaurant_favorites_table/migration.sql",
    "chars": 677,
    "preview": "-- CreateTable\nCREATE TABLE \"UserFavoriteRestaurant\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"restaura"
  },
  {
    "path": "prisma/migrations/20240503235713_add_compound_key_to_user_favorite_restaurant/migration.sql",
    "chars": 498,
    "preview": "/*\n  Warnings:\n\n  - The primary key for the `UserFavoriteRestaurant` table will be changed. If it partially fails, the t"
  },
  {
    "path": "prisma/migrations/migration_lock.toml",
    "chars": 126,
    "preview": "# Please do not edit this file manually\n# It should be added in your version-control system (i.e. Git)\nprovider = \"postg"
  },
  {
    "path": "prisma/schema.prisma",
    "chars": 4074,
    "preview": "generator client {\n  provider = \"prisma-client-js\"\n}\n\ndatasource db {\n  provider = \"postgresql\"\n  url      = env(\"DATABA"
  },
  {
    "path": "prisma/seed.ts",
    "chars": 24964,
    "preview": "const { PrismaClient } = require(\"@prisma/client\");\n\nconst prismaClient = new PrismaClient();\n\nconst description =\n  \"Lo"
  },
  {
    "path": "tailwind.config.ts",
    "chars": 2179,
    "preview": "import type { Config } from \"tailwindcss\"\n\nconst config = {\n  darkMode: [\"class\"],\n  content: [\n    './pages/**/*.{ts,ts"
  },
  {
    "path": "tsconfig.json",
    "chars": 616,
    "preview": "{\n  \"compilerOptions\": {\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n  "
  }
]

About this extraction

This page contains the full source code of the felipemotarocha/fullstackweek-foods GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 76 files (124.0 KB), approximately 33.7k tokens, and a symbol index with 48 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!