diff --git a/package-lock.json b/package-lock.json index 8510860..80f86ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,13 @@ "version": "0.1.0", "dependencies": { "@auth/prisma-adapter": "^1.0.4", + "@hookform/resolvers": "^3.3.4", "@prisma/client": "^5.4.2", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", @@ -26,10 +29,12 @@ "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.51.2", "sonner": "^1.4.41", "stripe": "^14.1.0", "tailwind-merge": "^1.14.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20", @@ -165,6 +170,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz", + "integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -535,6 +548,36 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz", + "integrity": "sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collapsible": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", @@ -765,6 +808,29 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz", + "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-portal": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", @@ -977,6 +1043,41 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", + "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", + "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz", @@ -4279,6 +4380,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.51.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.2.tgz", + "integrity": "sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5304,6 +5420,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index c54c526..75c2863 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,13 @@ }, "dependencies": { "@auth/prisma-adapter": "^1.0.4", + "@hookform/resolvers": "^3.3.4", "@prisma/client": "^5.4.2", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", @@ -32,10 +35,12 @@ "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.51.2", "sonner": "^1.4.41", "stripe": "^14.1.0", "tailwind-merge": "^1.14.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20", diff --git a/src/actions/Wishlist.ts b/src/actions/Wishlist.ts index 2de9e0d..71c6e38 100644 --- a/src/actions/Wishlist.ts +++ b/src/actions/Wishlist.ts @@ -1,6 +1,8 @@ "use server"; +import { authOptions } from "@/lib/auth"; import { prismaClient } from "@/lib/prisma"; +import { getServerSession } from "next-auth"; export const addProductToWishlist = async ( userId: string, @@ -48,3 +50,16 @@ export const addProductToWishlist = async ( }, }); }; + +export const getUserWishlist = async()=>{ + const session = await getServerSession(authOptions); + if (!session || !session.user) { + return [] + } + const userWishlists = await prismaClient.wishList.findMany({ + where:{ + userId: session.user.id + } + }) + return userWishlists +} \ No newline at end of file diff --git a/src/app/(shop)/product/[slug]/components/WishButton.tsx b/src/app/(shop)/product/[slug]/components/WishButton.tsx index 90ccdb7..2c46534 100644 --- a/src/app/(shop)/product/[slug]/components/WishButton.tsx +++ b/src/app/(shop)/product/[slug]/components/WishButton.tsx @@ -1,74 +1,208 @@ "use client"; -import { addProductToWishlist } from "@/actions/Wishlist"; +import { addProductToWishlist, getUserWishlist } from "@/actions/Wishlist"; import { StarIcon } from "lucide-react"; import { signIn, useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; import LoadingButton from "../../../../../components/ui/loading-button"; import { WishList } from "@prisma/client"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/form"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Button } from "@/components/ui/button"; +import error from "next/error"; interface WishButtonProps { productId: string; wishLists: WishList[]; } +const wishListFormSchema = z.object({ + wishlists: z.array(z.string()).refine((value) => value.some((item) => item), { + message: "You have to select at least one item.", + }), +}); + +type WishListFormData = z.infer; const WishButton = ({ productId, wishLists }: WishButtonProps) => { + const [dialogIsOpen, setDialogIsOpen] = useState(false); const { data: session } = useSession(); + const form = useForm({ + resolver: zodResolver(wishListFormSchema), + defaultValues: { + wishlists: [], + }, + }); + const router = useRouter(); + const disableButton = !session || !session.user; const [loading, setLoading] = useState(false); - const handleAddToWishlist = async () => { + const handleAddToWishlist = async (data: WishListFormData) => { setLoading(true); - if (!session || !session.user) { - toast("Você precisa estar logado para adicionar aos favoritos", { + try { + if (!session || !session.user) { + toast("Você precisa estar logado para adicionar aos favoritos", { + action: { + label: "Login", + onClick: () => { + signIn(); + }, + }, + classNames: { + toast: "bg-red-500 border-red-500", + title: "text-white", + actionButton: "!text-red-500 !bg-white font-bold", + }, + }); + + setLoading(false); + return; + } + + data.wishlists.forEach((wishlistId) => { + addProductToWishlist(session.user.id, productId, wishlistId); + }); + + form.reset() + setDialogIsOpen(false); + router.refresh(); + + toast("Produto adicionado aos favoritos", { action: { - label: "Login", + label: "Ver favoritos", onClick: () => { - signIn(); + router.push("/wish-list"); }, }, + }); + } catch (error) { + console.log(error); + toast("Desculpe, algo deu errado! ;(", { classNames: { toast: "bg-red-500 border-red-500", title: "text-white", actionButton: "!text-red-500 !bg-white font-bold", }, }); - + } finally { setLoading(false); - return; } - - await addProductToWishlist(session.user.id, productId); - - router.refresh(); - - toast("Produto adicionado aos favoritos", { - action: { - label: "Ver favoritos", - onClick: () => { - router.push("/wish-list"); - }, - }, - }); - - setLoading(false); }; return ( - - 0 && "fill-white")} - /> - Favoritos - + + + + 0 && "fill-white")} + /> + Favoritos + + + + + Adicionar produto aos favoritos + + Selecione em qual lista você deseja favoritar esse produto. + + +
+ + ( + + {wishLists.map((wishlist) => ( + { + return ( + + + { + return checked + ? field.onChange([ + ...field.value, + wishlist.id, + ]) + : field.onChange( + field.value?.filter( + (value) => value !== wishlist.id, + ), + ); + }} + /> + + + {wishlist.name} + + + ); + }} + /> + ))} + + )} + /> + + + + Salvar + + + + + + + +
+
); }; diff --git a/src/app/(shop)/product/[slug]/components/product-info.tsx b/src/app/(shop)/product/[slug]/components/product-info.tsx index b61e806..cff81a1 100644 --- a/src/app/(shop)/product/[slug]/components/product-info.tsx +++ b/src/app/(shop)/product/[slug]/components/product-info.tsx @@ -8,6 +8,7 @@ import { ArrowLeftIcon, ArrowRightIcon, TruckIcon } from "lucide-react"; import { useContext, useState } from "react"; import WishButton from "./WishButton"; import { WishList } from "@prisma/client"; +import { useSession } from "next-auth/react"; interface ProductWithTotalPriceAndWishLists extends ProductWithTotalPrice { wishLists: WishList[]; @@ -15,13 +16,14 @@ interface ProductWithTotalPriceAndWishLists extends ProductWithTotalPrice { interface ProductInfoProps { product: ProductWithTotalPriceAndWishLists; + userWishLists : WishList[]; } -const ProductInfo = ( { product } : ProductInfoProps) => { - console.log(product.wishLists); +const ProductInfo = ( { product,userWishLists } : ProductInfoProps) => { const [quantity, setQuantity] = useState(1); const { addProductToCart } = useContext(CartContext); + const { data } = useSession(); const handleDecreaseQuantityClick = () => { setQuantity((prev) => (prev === 1 ? prev : prev - 1)); @@ -82,7 +84,7 @@ const ProductInfo = ( { product } : ProductInfoProps) => {
- +
diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..df61a13 --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..01ff19c --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..4603f8b --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +