From 314a51471d685f51e6ba8869c7f3518b1fce048a Mon Sep 17 00:00:00 2001 From: franco sanchez Date: Fri, 13 Dec 2024 12:31:17 -0300 Subject: [PATCH] feat: you can now add books to collections from within the Book.tsx view --- src/components/cards/RelatedCard.tsx | 4 +- .../modals/ModalCollectionSelector.tsx | 179 ++++++++++++++++++ src/components/modals/ModalOptions.tsx | 7 +- src/components/nav/DesktopNav.tsx | 39 ++-- src/components/nav/MobileNav.tsx | 49 +++-- src/components/nav/menu/MenuProfile.tsx | 11 +- src/components/skeletons/SkeletonDBook.tsx | 1 + src/components/types.d.ts | 14 ++ src/constant/constants.ts | 7 +- src/hooks/queries.ts | 37 ++++ src/pages/Book.tsx | 51 ++++- .../profile/collections/AllCollections.tsx | 72 +++---- src/routes.tsx | 12 +- src/services/api.ts | 30 +++ src/utils/utils.ts | 2 + 15 files changed, 410 insertions(+), 105 deletions(-) create mode 100644 src/components/modals/ModalCollectionSelector.tsx diff --git a/src/components/cards/RelatedCard.tsx b/src/components/cards/RelatedCard.tsx index 1d8167e..84630fd 100644 --- a/src/components/cards/RelatedCard.tsx +++ b/src/components/cards/RelatedCard.tsx @@ -10,7 +10,8 @@ export function RelatedCard({ title, authors, pathUrl, refetchQueries }: CardTyp const handleEnterKey = useHandleEnterKey(pathUrl); const borderCard = useColorModeValue('gray.200', 'gray.600'); const colorAuthorCard = useColorModeValue('gray.600', 'gray.300'); - const bgRandomBookCardLink = useColorModeValue('gray.300', 'black'); + const colorLink = useColorModeValue('green.900', 'green.50'); + const bgRandomBookCardLink = useColorModeValue('green.50', 'green.900'); const outlineCard = useColorModeValue('black', 'white'); return ( @@ -65,6 +66,7 @@ export function RelatedCard({ title, authors, pathUrl, refetchQueries }: CardTyp bg={bgRandomBookCardLink} py='4' px='7' + color={colorLink} position={{ base: 'initial', md: 'absolute' }} bottom='0' tabIndex={-1} diff --git a/src/components/modals/ModalCollectionSelector.tsx b/src/components/modals/ModalCollectionSelector.tsx new file mode 100644 index 0000000..a4d9d4d --- /dev/null +++ b/src/components/modals/ModalCollectionSelector.tsx @@ -0,0 +1,179 @@ +import React, { useEffect, useState } from 'react'; +import { + Box, + Button, + Checkbox, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + Spinner, + useColorModeValue, + VStack, +} from '@chakra-ui/react'; +import { FaCheckCircle, FaExclamationCircle } from 'react-icons/fa'; + +import { ModalCollectionSelectorType } from '@components/types'; +import { useCollectionBooks } from '@hooks/queries'; +import { useMyToast } from '@hooks/useMyToast'; + +export function ModalCollectionSelector({ + userId, + bookId, + data, + isPending, + isOpen, + onClose, +}: ModalCollectionSelectorType) { + const [selectedCollections, setSelectedCollections] = useState<{ + [key: string]: boolean; + }>({}); + + const bgColorBox = useColorModeValue('white', 'gray.900'); + const myToast = useMyToast(); + const { mutate, isPending: isPendingMutate } = useCollectionBooks(); + + useEffect(() => { + // Inicializa el estado de selección basado en la propiedad 'checked' de los datos + const initialSelectedState = + data?.reduce( + (acc, collection) => { + acc[collection.id] = collection.checked || false; + return acc; + }, + {} as { [key: string]: boolean }, + ) || {}; + + setSelectedCollections(initialSelectedState); + }, [bookId, data]); + + function handleCheckboxChange(collectionId: string) { + setSelectedCollections((prev) => { + const newSelectedState = { + ...prev, + [collectionId]: !prev[collectionId], + }; + return newSelectedState; + }); + } + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + + if (!isPendingMutate) { + // Preparar los datos para el mutate + const collections = data?.map((collection) => ({ + collectionId: collection.id, + collectionName: collection.name, + isInCollection: selectedCollections[collection.id] || false, + })); + + const mutateData = { + userId, + collections, + bookId, + checked: true, + }; + + mutate(mutateData, { + onSuccess: () => { + // Obtener nombres de colecciones seleccionadas + const selectedCollectionNames = data + ?.filter((collection) => selectedCollections[collection.id]) + .map((collection) => collection.name) + .join(', '); + + myToast({ + title: `Se agregó a colección(es): ${selectedCollectionNames}`, + icon: FaCheckCircle, + iconColor: 'green.700', + bgColor: 'black', + width: '300px', + color: 'whitesmoke', + align: 'center', + padding: '1', + fntSize: 'md', + bxSize: 5, + }); + + onClose(); + }, + onError: () => { + myToast({ + title: 'Error al actualizar colecciones', + description: 'Ocurrió un problema al modificar las colecciones', + icon: FaExclamationCircle, + iconColor: 'red.700', + bgColor: 'black', + width: '300px', + color: 'whitesmoke', + align: 'center', + padding: '1', + fntSize: 'md', + bxSize: 5, + }); + }, + }); + } + } + + return ( + <> + + + + Agregar a colección + + + + + {isPending ? ( + + + + ) : ( + data?.map((collection) => ( + handleCheckboxChange(collection.id)} + colorScheme='green' + > + {collection.name} + + )) + )} + + + + + + + + ); +} diff --git a/src/components/modals/ModalOptions.tsx b/src/components/modals/ModalOptions.tsx index 7e94621..d0a7b66 100644 --- a/src/components/modals/ModalOptions.tsx +++ b/src/components/modals/ModalOptions.tsx @@ -22,7 +22,12 @@ export function ModalOptions({ return ( <> - + diff --git a/src/components/nav/DesktopNav.tsx b/src/components/nav/DesktopNav.tsx index e02765b..e058b6a 100644 --- a/src/components/nav/DesktopNav.tsx +++ b/src/components/nav/DesktopNav.tsx @@ -100,28 +100,23 @@ export function DesktopNav() { - {navLink - .filter(({ name }) => { - if (name === 'Mis favoritos' && !data) return false; - return true; - }) - .map(({ name, href }) => ( - - - {name} - - - ))} + {navLink.map(({ name, href }) => ( + + + {name} + + + ))} diff --git a/src/components/nav/MobileNav.tsx b/src/components/nav/MobileNav.tsx index c2bb668..c748061 100644 --- a/src/components/nav/MobileNav.tsx +++ b/src/components/nav/MobileNav.tsx @@ -197,33 +197,28 @@ export function MobileNav() { - {navLink - .filter(({ name }) => { - if (name === 'Mis favoritos' && !data) return false; - return true; - }) - .map(({ icon, name, href }) => ( - - - - {name} - - - ))} + {navLink.map(({ icon, name, href }) => ( + + + + {name} + + + ))} - Crear Publicación + Publicar - Mis Colecciones + Mis colecciones + + + Mis favoritos + = [ { name: 'Inicio', href: '/', icon: GrHome }, { name: 'Explorar', href: 'explore', icon: MdOutlineExplore }, { name: 'Más vistos', href: 'most-viewed', icon: ImEyePlus }, - { - name: 'Mis favoritos', - href: 'my-favorites', - icon: MdOutlineFavoriteBorder, - }, ]; const accountLinks: Array = [ diff --git a/src/hooks/queries.ts b/src/hooks/queries.ts index 8876c5f..f497993 100644 --- a/src/hooks/queries.ts +++ b/src/hooks/queries.ts @@ -32,6 +32,8 @@ import { deleteCollections, postCollections, patchCollectionsName, + getCollectionsForUser, + patchToggleBookInCollection, } from '@services/api'; import { useAccountActions } from '@hooks/useAccountActions'; import { keys } from '@utils/utils'; @@ -287,6 +289,27 @@ function useCreateCollections(userId: string | undefined) { }); } +function useCollectionBooks() { + return useMutation({ + mutationKey: [keys.collectionsBooks], + mutationFn: ({ + userId, + collections, + bookId, + checked, + }: { + userId: string | undefined; + collections: Array<{ + collectionId: string; + collectionName: string; + isInCollection: boolean; + }>; + bookId: string; + checked: boolean; + }) => patchToggleBookInCollection(userId, collections, bookId, checked), + }); +} + function useUpdateCollectionName( userId: string | undefined, collectionId: string | undefined, @@ -306,6 +329,18 @@ function useCollections(userId: string | undefined) { }); } +function useCollectionsForUser(userId: string | undefined, bookId: string) { + return useQuery({ + queryKey: [keys.allCollectionsForUser, userId, bookId], + queryFn: () => getCollectionsForUser(userId, bookId), + enabled: false, + refetchOnWindowFocus: false, + retry: false, + staleTime: 0, + gcTime: 0, + }); +} + function useDeleteCollections() { return useMutation({ mutationKey: [keys.deleteCollections], @@ -377,8 +412,10 @@ export { useMoreBooksAuthors, useFavoriteBook, useCollections, + useCollectionsForUser, useCollectionDetail, useCreateCollections, + useCollectionBooks, useUpdateCollectionName, useDeleteCollections, diff --git a/src/pages/Book.tsx b/src/pages/Book.tsx index b339c99..0b1548e 100644 --- a/src/pages/Book.tsx +++ b/src/pages/Book.tsx @@ -23,11 +23,17 @@ import { import { BsTag } from 'react-icons/bs'; import { FaCheckCircle } from 'react-icons/fa'; import { MdOutlineFavoriteBorder, MdOutlineFavorite } from 'react-icons/md'; +import { FaRegBookmark } from 'react-icons/fa6'; import LazyLoad from 'react-lazy-load'; import Atropos from 'atropos/react'; import 'atropos/css'; -import { useBook, useFavoriteBook, useDeleteBook } from '@hooks/queries'; +import { + useBook, + useFavoriteBook, + useDeleteBook, + useCollectionsForUser, +} from '@hooks/queries'; import { handleImageLoad } from '@utils/utils'; import { MainHead } from '@components/layout/Head'; import { MyTag } from '@components/ui/MyTag'; @@ -38,6 +44,7 @@ import { BooksSection } from '@components/BooksSection'; import { ImageZoom } from '@components/ui/ImageZoom'; import { ModalOptions } from '@components/modals/ModalOptions'; import { ModalConfirmation } from '@components/modals/ModalConfirmation'; +import { ModalCollectionSelector } from '@components/modals/ModalCollectionSelector'; import { ModalForm } from '@components/modals/ModalForm'; import { useAuth } from '@contexts/AuthContext'; import { useMyToast } from '@hooks/useMyToast'; @@ -80,9 +87,15 @@ export default function Book() { onOpen: onOpenShare, onClose: onCloseShare, } = useDisclosure(); + const { + isOpen: isOpenCollectionSelector, + onOpen: onOpenCollectionSelector, + onClose: onCloseCollectionSelector, + } = useDisclosure(); let uiLink; let btnMoreOptions; let btnFavorite; + let btnCollection; const { data } = useBook(pathUrl, getToken); let bookObject = data; @@ -94,6 +107,11 @@ export default function Book() { bookObject = data; } + const { + data: collections, + refetch, + isPending: isPendingCollections, + } = useCollectionsForUser(currentUser?.uid, bookObject.id); const [isFavorite, setIsFavorite] = useState(bookObject.isFavorite); const { mutate: mutateFavorite, isSuccess: successFavorite } = useFavoriteBook( bookObject.id, @@ -170,6 +188,28 @@ export default function Book() { ); + + btnCollection = ( + + + + ); } if (successDelete) { @@ -260,6 +300,7 @@ export default function Book() { {btnFavorite} + {btnCollection} {btnMoreOptions} @@ -272,6 +313,14 @@ export default function Book() { onCloseOptions(); }} /> + {data?.collections.map(({ id, name, createdAt }) => ( - - {/* */} + <> - + - + {name} - {parseDate(createdAt)} + {parseDate(createdAt)} - + + Abrir + + + - {/* */} - + ))} ); diff --git a/src/routes.tsx b/src/routes.tsx index 68b4b63..59278d8 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -49,10 +49,6 @@ export const routes = createBrowserRouter([ path: '/most-viewed', element: , }, - { - path: '/my-favorites', - element: , - }, { path: '/new-post', element: ( @@ -125,6 +121,14 @@ export const routes = createBrowserRouter([ ), }, + { + path: '/my-favorites', + element: ( + + + + ), + }, { path: '/my-collections', element: ( diff --git a/src/services/api.ts b/src/services/api.ts index 5b6efaa..edeb87f 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -75,6 +75,12 @@ async function getFindAllCollections(userId: string | undefined) { return await fetchData(`${API_URL}/users/${userId}/my-collections`); } +async function getCollectionsForUser(userId: string | undefined, bookId: string) { + return await fetchData( + `${API_URL}/users/${userId}/my-collections/summary/${bookId}`, + ); +} + async function postCollections(userId: string | undefined, body: any) { return await fetchData(`${API_URL}/users/${userId}/my-collections`, { method: 'POST', @@ -83,6 +89,28 @@ async function postCollections(userId: string | undefined, body: any) { }); } +async function patchToggleBookInCollection( + userId: string | undefined, + collections: Array<{ + collectionId: string; + collectionName: string; + isInCollection: boolean; + }>, + bookId: string, + checked: boolean, +) { + return await fetchData(`${API_URL}/users/my-collections/books/toggle`, { + method: 'PATCH', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + userId, + collections, + bookId, + checked, + }), + }); +} + async function patchCollectionsName( userId: string | undefined, collectionId: string | undefined, @@ -187,8 +215,10 @@ export { getRelatedBooks, getMoreBooksAuthors, patchToggleFavorite, + getCollectionsForUser, getFindAllCollections, postCollections, + patchToggleBookInCollection, patchCollectionsName, deleteCollections, getFindOneCollection, diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 9785578..8fa1908 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -25,7 +25,9 @@ const keys = { userFavoriteBooks: 'UserFavoriteBooks', createCollections: 'CreateCollections', updateCollectionsName: 'UpdateCollectionsName', + collectionsBooks: 'CollectionsBooks', allCollections: 'AllCollections', + allCollectionsForUser: 'AllCollectionsForUser', collectionsDetail: 'CollectionsDetail', deleteCollections: 'DeleteCollections', deleteAccount: 'DeleteAccount',