From 61a2ebdd80c168af4c55d97e43d09fe8e141e857 Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Sat, 10 May 2025 08:22:48 -0400 Subject: [PATCH 01/50] fix in dimensions, and size responsive --- package-lock.json | 4 +- src/app/(shop)/search/page.tsx | 2 +- src/components/CardButton.tsx | 108 ++++----- src/components/Product/CardBase.tsx | 121 ---------- src/components/Product/ProductCard.tsx | 248 ++++----------------- src/components/Product/ProductCarousel.tsx | 147 ++++-------- 6 files changed, 154 insertions(+), 476 deletions(-) delete mode 100644 src/components/Product/CardBase.tsx diff --git a/package-lock.json b/package-lock.json index abd50b1..5e63970 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ecommerce", - "version": "0.3.1", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ecommerce", - "version": "0.3.1", + "version": "0.4.0", "dependencies": { "@headlessui/react": "^2.2.0", "@heroicons/react": "^2.2.0", diff --git a/src/app/(shop)/search/page.tsx b/src/app/(shop)/search/page.tsx index 57a1c85..7854e85 100644 --- a/src/app/(shop)/search/page.tsx +++ b/src/app/(shop)/search/page.tsx @@ -279,7 +279,7 @@ export default function SearchPage() {
{displayProducts.map((p) => ( - + ))}
diff --git a/src/components/CardButton.tsx b/src/components/CardButton.tsx index a31c157..4e8369f 100644 --- a/src/components/CardButton.tsx +++ b/src/components/CardButton.tsx @@ -1,4 +1,5 @@ 'use client'; + import React from 'react'; import { Colors, FontSizes } from '@/styles/styles'; import { useCart } from '@/context/CartContext'; @@ -29,6 +30,11 @@ type CardButtonProps = ProductModeProps | FallbackModeProps; const CardButton: React.FC = (props) => { const { cartItems, addItem, updateItemQuantity, removeItem } = useCart(); + + const buttonStyle = { + backgroundColor: Colors.primary, + }; + if ('product' in props && props.product) { const { product, className } = props; const compositeId = `${product.productPresentationId}`; @@ -78,24 +84,25 @@ const CardButton: React.FC = (props) => { return (
{quantity === 0 ? (
- + + +
) : ( <>
- - + –
= (props) => {
- + + +
)}
); - } else { - const { quantity, onAdd, onSubtract, className } = - props as FallbackModeProps; - const defaultContainerStyles = - quantity === 0 - ? 'w-[48px] h-[48px] rounded-full' - : 'w-[129px] h-[48px] rounded-full px-[25px]'; - const containerStyles = className ? className : defaultContainerStyles; + } - return ( -
- {quantity === 0 ? ( + // fallback mode + const { quantity, onAdd, onSubtract, className } = props as FallbackModeProps; + const containerStyles = + className || + (quantity === 0 + ? 'w-[48px] h-[48px] rounded-full' + : 'w-[129px] h-[48px] rounded-full px-[25px]'); + + return ( +
+ {quantity === 0 ? ( +
+ + +
+ ) : ( + <>
+ – +
+
- + + {quantity} +
+
+ +
- ) : ( - <> -
- - -
-
- {quantity} -
-
- + -
- - )} -
- ); - } + + )} +
+ ); }; export default CardButton; diff --git a/src/components/Product/CardBase.tsx b/src/components/Product/CardBase.tsx deleted file mode 100644 index 0d4ab8e..0000000 --- a/src/components/Product/CardBase.tsx +++ /dev/null @@ -1,121 +0,0 @@ -'use client'; -import type React from 'react'; -import Image, { type StaticImageData } from 'next/image'; -import Link from 'next/link'; -import { Colors, CardDimensions, imageSizes } from '@/styles/styles'; - -export type ImageType = string | StaticImageData; - -interface CardBaseProps { - variant?: 'regular' | 'minimal' | 'responsive'; - showRibbon?: boolean; - imageSrc?: string | ImageType; - ribbonText?: string; - label?: string; - imageLink?: - | string - | { - pathname: string; - query: Record; - }; - children: React.ReactNode; -} - -const CardBase: React.FC = ({ - variant = 'regular', - showRibbon = false, - imageSrc = '', - ribbonText, - label, - imageLink, - children, -}) => { - const cardSize = CardDimensions.cardSizes[variant]; - const ribbonDim = CardDimensions.ribbonDimensions[variant]; - const currentImageSize = imageSizes[variant]; - - return ( -
- {/* Ribbon (se muestra solo en variantes que no sean 'responsive') */} - {variant !== 'responsive' && showRibbon && ribbonText && ( -
- - - -
- - {ribbonText.split(' ').join('\n')} - -
-
- )} - - {/* Etiqueta */} - {label && ( -
- {label} -
- )} - - {/* Contenedor de la imagen */} -
- {imageSrc ? ( - imageLink ? ( - // Envolvemos la imagen en un Link para que sea clickeable - -
- Product Image -
- - ) : ( -
- Product Image -
- ) - ) : ( - Sin imagen - )} -
- - {/* Contenido dinámico */} - {children} -
- ); -}; - -export default CardBase; diff --git a/src/components/Product/ProductCard.tsx b/src/components/Product/ProductCard.tsx index 7f461a0..6a522e5 100644 --- a/src/components/Product/ProductCard.tsx +++ b/src/components/Product/ProductCard.tsx @@ -1,214 +1,64 @@ 'use client'; -import React from 'react'; -import CardBase from './CardBase'; -import { Colors, FontSizes } from '@/styles/styles'; -import CardButton from '../CardButton'; + +import Image from 'next/image'; import Link from 'next/link'; -import { ProductPresentation } from '@pharmatech/sdk'; +import type { ProductPresentation } from '@pharmatech/sdk'; +import CardButton from '../CardButton'; -export type ProductCardProps = { +type Props = { product: ProductPresentation; - ribbonText?: string; - label?: string; - lastPrice?: number; - discountPercentage?: number; - variant?: 'regular' | 'minimal' | 'responsive'; }; -const ProductCard: React.FC = ({ - product, - ribbonText, - label, - lastPrice, - discountPercentage, - variant, -}) => { - const detailLink = { - pathname: `/product/${product.product.id}/presentation/${product.presentation.id}`, - query: { productPresentationId: product.id }, - }; - const imageSrc = product.product.images?.[0]?.url ?? ''; +export default function ProductCard({ product }: Props) { + const name = `${product.product.name} ${product.presentation.name}`; + const image = product.product.images?.[0]?.url || '/placeholder.svg'; + const stock = product.stock ?? 0; + const price = product.price; + const href = `/product/${product.product.id}/presentation/${product.presentation.id}`; + return ( -
- -
- {variant === 'responsive' && ( -
- -
- )} - -

- {product.product.name} -

-

- Stock: {product.stock || 0} -

+
+ + {name} + + + {/* Contenido */} +
+
+ +

+ {name} +

-
-
- {lastPrice !== undefined && ( -
-

- ${lastPrice.toFixed(2)} -

- {discountPercentage !== undefined && ( -
- -{discountPercentage}% -
- )} -
- )} +

Stock: {stock}

+
-

- ${product.price.toFixed(2)} -

-
+
+ + ${price.toFixed(2)} + - {variant !== 'responsive' && ( - - )} +
+
- +
); -}; - -export default ProductCard; +} diff --git a/src/components/Product/ProductCarousel.tsx b/src/components/Product/ProductCarousel.tsx index 65485b8..3d17737 100644 --- a/src/components/Product/ProductCarousel.tsx +++ b/src/components/Product/ProductCarousel.tsx @@ -1,125 +1,66 @@ 'use client'; -import { useState, useRef, useEffect } from 'react'; +import { useRef } from 'react'; import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/solid'; -import ProductCard from '@/components/Product/ProductCard'; -import { Colors } from '@/styles/styles'; -import { ProductPresentation } from '@pharmatech/sdk'; +import ProductCard from './ProductCard'; +import type { ProductPresentation } from '@pharmatech/sdk'; -export type ProductSliderProps = { +interface ProductSliderProps { title?: string; products: ProductPresentation[]; - carouselType?: 'regular' | 'large'; -}; +} -export default function ProductSlider({ - title, - products, - carouselType = 'large', -}: ProductSliderProps) { +export default function ProductSlider({ title, products }: ProductSliderProps) { const sliderRef = useRef(null); - const [visibleProducts, setVisibleProducts] = useState(3); - const [variant, setVariant] = useState<'responsive' | 'minimal' | 'regular'>( - 'regular', - ); - - useEffect(() => { - const handleResize = () => { - const width = window.innerWidth; - if (width < 640) { - setVariant('responsive'); - setVisibleProducts(1); - } else if (width < 1111) { - setVariant('minimal'); - setVisibleProducts(2); - } else if (width < 1900) { - setVariant('regular'); - setVisibleProducts(3); - } else { - setVariant('regular'); - setVisibleProducts(4); - } - }; - - handleResize(); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, [carouselType]); const scroll = (direction: 'left' | 'right') => { if (!sliderRef.current) return; - const containerWidth = sliderRef.current.clientWidth; - const itemWidth = containerWidth / visibleProducts; - const scrollValue = direction === 'left' ? -itemWidth : itemWidth; - sliderRef.current.scrollBy({ left: scrollValue, behavior: 'smooth' }); + const scrollAmount = 276; // card width (260) + gap (16) + sliderRef.current.scrollBy({ + left: direction === 'left' ? -scrollAmount : scrollAmount, + behavior: 'smooth', + }); }; - // Para mobile se usa un margen y gap menor - const marginLR = variant === 'responsive' ? '16px' : '56px'; - const gapBetween = variant === 'responsive' ? 'gap-2' : 'gap-4'; - // Define la altura de la card en mobile; ajusta este valor según tu diseño. - //const cardHeight = variant === 'responsive' ? '400px' : 'auto'; - return ( -
- {' '} - {/* Cambiamos mt-[-10%] a mt-8 */} +
{title &&

{title}

} -
- {/* Flecha izquierda */} -
- -
- {/* Carrusel */} -
- {products.map((product) => ( -
- -
- ))} -
+ - {/* Flecha derecha */} -
- -
+ +
+ ))}
+ + ); } From e425749b517d4e005a337648fcde935e84520de1 Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Sat, 10 May 2025 08:39:44 -0400 Subject: [PATCH 02/50] fixex scrollbar --- src/components/CardButton.tsx | 2 +- src/components/Product/ProductCard.tsx | 8 ++++---- src/components/Product/ProductCarousel.tsx | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/CardButton.tsx b/src/components/CardButton.tsx index 4e8369f..4d63b90 100644 --- a/src/components/CardButton.tsx +++ b/src/components/CardButton.tsx @@ -78,7 +78,7 @@ const CardButton: React.FC = (props) => { const defaultContainerStyles = quantity === 0 ? 'w-[48px] h-[48px] rounded-full' - : 'w-[129px] h-[48px] rounded-full px-[25px]'; + : 'w-[120px] h-[48px] rounded-full px-[25px]'; const containerStyles = className ? className : defaultContainerStyles; diff --git a/src/components/Product/ProductCard.tsx b/src/components/Product/ProductCard.tsx index 6a522e5..16bc48c 100644 --- a/src/components/Product/ProductCard.tsx +++ b/src/components/Product/ProductCard.tsx @@ -32,16 +32,16 @@ export default function ProductCard({ product }: Props) {
-

+

{name}

-

Stock: {stock}

+

Stock: {stock}

-
- +
+ ${price.toFixed(2)} diff --git a/src/components/Product/ProductCarousel.tsx b/src/components/Product/ProductCarousel.tsx index 3d17737..b3c3d83 100644 --- a/src/components/Product/ProductCarousel.tsx +++ b/src/components/Product/ProductCarousel.tsx @@ -15,7 +15,7 @@ export default function ProductSlider({ title, products }: ProductSliderProps) { const scroll = (direction: 'left' | 'right') => { if (!sliderRef.current) return; - const scrollAmount = 276; // card width (260) + gap (16) + const scrollAmount = 276; sliderRef.current.scrollBy({ left: direction === 'left' ? -scrollAmount : scrollAmount, behavior: 'smooth', @@ -36,7 +36,7 @@ export default function ProductSlider({ title, products }: ProductSliderProps) {
Date: Sat, 10 May 2025 08:47:43 -0400 Subject: [PATCH 03/50] prop carousel type unnecesary --- src/app/(shop)/page.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/app/(shop)/page.tsx b/src/app/(shop)/page.tsx index 88556b4..936ac22 100644 --- a/src/app/(shop)/page.tsx +++ b/src/app/(shop)/page.tsx @@ -132,7 +132,7 @@ export default function Home() { Productos en Oferta Exclusiva
- +
{/* Sección recomendados */} @@ -142,10 +142,7 @@ export default function Home() { Productos Recomendados para ti
- +
)} From bdb436db41256a2d85126427abe4b3f588bd123c Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Sat, 10 May 2025 08:56:02 -0400 Subject: [PATCH 04/50] remove prop carousel --- .../product/[productId]/presentation/[presentationId]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx index a2aa1b5..1e09593 100644 --- a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx +++ b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx @@ -184,7 +184,7 @@ export default function ProductDetailPage() {

Productos de la marca {genericProduct.manufacturer.name}

- +
); From 587fc79fde1031bc96cd6f1ec3973fd34f985ced Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Wed, 14 May 2025 16:04:47 -0400 Subject: [PATCH 05/50] Listen order updated event to request order data --- src/app/(shop)/order/[id]/page.tsx | 49 ++++++++++++------------------ 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/app/(shop)/order/[id]/page.tsx b/src/app/(shop)/order/[id]/page.tsx index 6b566b2..57bfad7 100644 --- a/src/app/(shop)/order/[id]/page.tsx +++ b/src/app/(shop)/order/[id]/page.tsx @@ -28,40 +28,31 @@ export default function OrderInProgress() { const id = params?.id; const router = useRouter(); const { token } = useAuth(); - const [order, setOrder] = useState(); - const [isConnected, setIsConnected] = useState(false); - - useEffect(() => { - const socket = io(SOCKET_URL, { - transportOptions: { - polling: { - extraHeaders: { - authorization: `Bearer ${token}`, - }, + const socket = io(SOCKET_URL, { + autoConnect: false, + transportOptions: { + polling: { + extraHeaders: { + authorization: `Bearer ${token}`, }, }, - }); + }, + }); + const [order, setOrder] = useState(); + const [orderStatus, setOrderStatus] = useState(OrderStatus.REQUESTED); - function onConnect() { - setIsConnected(true); - console.log('Connected to socket: ', isConnected); - socket.on('order', (order: OrderDetailedResponse) => { - setOrder(order); - }); + useEffect(() => { + function onOrderUpdated(order: { orderId: string; status: OrderStatus }) { + setOrderStatus(order.status); } - function onDisconnect() { - setIsConnected(false); - } + socket.connect(); + socket.on('orderUpdated', onOrderUpdated); - if (id && token) { - socket.on('connect', onConnect); - socket.on('disconnect', onDisconnect); - return () => { - socket.off('connect', onConnect); - socket.off('disconnect', onDisconnect); - }; - } + return () => { + socket.off('orderUpdated', onOrderUpdated); + socket.disconnect(); + }; }, []); useEffect(() => { @@ -75,7 +66,7 @@ export default function OrderInProgress() { router.push('/checkout'); }); } - }, [id, token, router]); + }, [id, token, router, orderStatus]); const steps = useMemo(() => { if (!order) return ['Opciones de Compra']; From 5fa76cc874869a62515eebc3e764bf8a7238ed3b Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Fri, 16 May 2025 01:41:58 -0400 Subject: [PATCH 06/50] added discount and badge categories --- src/components/Product/ProductCard.tsx | 89 ++++++++++++++++++---- src/components/Product/ProductCarousel.tsx | 29 ++++--- src/lib/types/ExtendedProduct.ts | 10 +++ 3 files changed, 101 insertions(+), 27 deletions(-) create mode 100644 src/lib/types/ExtendedProduct.ts diff --git a/src/components/Product/ProductCard.tsx b/src/components/Product/ProductCard.tsx index 16bc48c..7994542 100644 --- a/src/components/Product/ProductCard.tsx +++ b/src/components/Product/ProductCard.tsx @@ -2,25 +2,61 @@ import Image from 'next/image'; import Link from 'next/link'; -import type { ProductPresentation } from '@pharmatech/sdk'; +import type { ExtendedProduct } from '@/lib/types/ExtendedProduct'; import CardButton from '../CardButton'; +import Badge from '../Badge'; type Props = { - product: ProductPresentation; + product: ExtendedProduct; }; export default function ProductCard({ product }: Props) { - const name = `${product.product.name} ${product.presentation.name}`; - const image = product.product.images?.[0]?.url || '/placeholder.svg'; const stock = product.stock ?? 0; + if (stock < 0) return null; + + const name = `${product.product.name} ${product.presentation.name}`; + const imageUrl = product.product.images?.[0]?.url || '/placeholder.svg'; const price = product.price; + const now = new Date(); + + const promosArray = Array.isArray(product.promo) + ? product.promo + : product.promo + ? [product.promo] + : []; + + const activePromos = promosArray.filter((promo) => { + const start = new Date(promo.startAt); + const end = new Date(promo.expiredAt); + return start <= now && now < end; + }); + + const totalDiscount = activePromos.reduce( + (sum, promo) => sum + (promo.discount || 0), + 0, + ); + const hasDiscount = totalDiscount > 0; + const finalPrice = hasDiscount ? price * (1 - totalDiscount / 100) : price; + + const firstCategory = product.product.categories?.[0]; const href = `/product/${product.product.id}/presentation/${product.presentation.id}`; return ( -
- +
+ {firstCategory && ( +
+ + {firstCategory.name} + +
+ )} + + {name} - {/* Contenido */}
@@ -36,14 +71,38 @@ export default function ProductCard({ product }: Props) { {name} - -

Stock: {stock}

+

+ Existencias: {stock} +

- - ${price.toFixed(2)} - +
+ {hasDiscount ? ( + <> +
+ + ${price.toFixed(2)} + + + -{totalDiscount}% + +
+ + ${finalPrice.toFixed(2)} + + + ) : ( + + ${price.toFixed(2)} + + )} +
diff --git a/src/components/Product/ProductCarousel.tsx b/src/components/Product/ProductCarousel.tsx index b3c3d83..86130ae 100644 --- a/src/components/Product/ProductCarousel.tsx +++ b/src/components/Product/ProductCarousel.tsx @@ -3,11 +3,11 @@ import { useRef } from 'react'; import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/solid'; import ProductCard from './ProductCard'; -import type { ProductPresentation } from '@pharmatech/sdk'; +import type { ExtendedProduct } from '@/lib/types/ExtendedProduct'; interface ProductSliderProps { title?: string; - products: ProductPresentation[]; + products: ExtendedProduct[]; } export default function ProductSlider({ title, products }: ProductSliderProps) { @@ -23,7 +23,7 @@ export default function ProductSlider({ title, products }: ProductSliderProps) { }; return ( -
+
{title &&

{title}

}
); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 3e66406..d7ee84e 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,9 +1,13 @@ 'use client'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import Link from 'next/link'; import Image from 'next/image'; -import { ShoppingCartIcon, UserCircleIcon } from '@heroicons/react/24/outline'; +import { + ShoppingCartIcon, + UserCircleIcon, + BellIcon, +} from '@heroicons/react/24/outline'; import Avatar from '@/components/Avatar'; import SearchBar from '@/components/SearchBar'; import { useRouter } from 'next/navigation'; @@ -15,6 +19,7 @@ import { useAuth } from '@/context/AuthContext'; import { api } from '@/lib/sdkConfig'; import { CategoryResponse, Pagination } from '@pharmatech/sdk'; import CartOverlay from './Cart/CartOverlay'; +import NotificationList from '@/components/User/NotificationList'; interface UserProfile { firstName: string; @@ -31,6 +36,8 @@ type NavBarProps = { export default function NavBar({ onCartClick }: NavBarProps) { const router = useRouter(); const [categories, setCategories] = useState([]); + const [isNotificationsOpen, setIsNotificationsOpen] = useState(false); + const notificationsRef = useRef(null); const { cartItems } = useCart(); const { token, user } = useAuth(); @@ -43,16 +50,13 @@ export default function NavBar({ onCartClick }: NavBarProps) { api.category .findAll({ page: 1, limit: 20 }) .then((resp: Pagination) => { - if (resp && resp.results) { - setCategories(resp.results); - } + if (resp?.results) setCategories(resp.results); }) .catch((err: unknown) => { console.error('Error al cargar categorías:', err); }); }, []); - // Obtener perfil si está logueado useEffect(() => { if (!token || !user?.sub) { setIsLoggedIn(false); @@ -66,7 +70,6 @@ export default function NavBar({ onCartClick }: NavBarProps) { try { const profileResponse = await api.user.getProfile(user.sub, token); setUserData(profileResponse); - console.log('Perfil obtenido:', profileResponse); } catch (error) { console.error('Error al obtener perfil:', error); setUserData(null); @@ -74,6 +77,20 @@ export default function NavBar({ onCartClick }: NavBarProps) { })(); }, [token, user]); + // Cerrar dropdown si se hace click fuera + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + notificationsRef.current && + !notificationsRef.current.contains(event.target as Node) + ) { + setIsNotificationsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + const handleSearch = (query: string, category: string) => { console.log('Buscando:', query, 'en', category); }; @@ -86,8 +103,9 @@ export default function NavBar({ onCartClick }: NavBarProps) { <> {/* Cart Overlay */} setIsCartOpen(false)} /> + {/* Versión Desktop */} -
{displayProducts.map((p) => ( - + ))}
From 4e178aa89265078fdfb8da04f0714e99b75f0d90 Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Sat, 17 May 2025 09:39:31 -0400 Subject: [PATCH 09/50] expected type fix --- src/app/(shop)/page.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/(shop)/page.tsx b/src/app/(shop)/page.tsx index 936ac22..d53798b 100644 --- a/src/app/(shop)/page.tsx +++ b/src/app/(shop)/page.tsx @@ -9,6 +9,7 @@ import Banner3 from '@/lib/utils/images/banner_final.jpg'; import { toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import ProductDetailImg from '@/lib/utils/images/Antibioticos.png'; +import { ExtendedProduct } from '@/lib/types/ExtendedProduct'; import type { Category as SDKCategory, @@ -132,7 +133,9 @@ export default function Home() { Productos en Oferta Exclusiva
- +
{/* Sección recomendados */} @@ -142,7 +145,9 @@ export default function Home() { Productos Recomendados para ti
- +
)} From 85879641e3de7600e7c080c3cee70881319b7cbd Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sat, 17 May 2025 11:32:12 -0400 Subject: [PATCH 10/50] Upgrade sdk version and fix custom product type --- package-lock.json | 8 ++++---- package.json | 2 +- src/app/(shop)/page.tsx | 9 ++------- .../[productId]/presentation/[presentationId]/page.tsx | 5 +---- src/app/(shop)/search/page.tsx | 6 +----- src/components/Product/ProductCard.tsx | 4 ++-- src/components/Product/ProductCarousel.tsx | 4 ++-- src/lib/types/ExtendedProduct.ts | 10 ---------- 8 files changed, 13 insertions(+), 35 deletions(-) delete mode 100644 src/lib/types/ExtendedProduct.ts diff --git a/package-lock.json b/package-lock.json index 5e63970..a4a795a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@headlessui/react": "^2.2.0", "@heroicons/react": "^2.2.0", "@next/third-parties": "^15.3.0", - "@pharmatech/sdk": "^0.4.16", + "@pharmatech/sdk": "^0.4.18", "@radix-ui/react-slider": "^1.2.4", "@react-google-maps/api": "^2.20.6", "@types/google.maps": "^3.58.1", @@ -1921,9 +1921,9 @@ } }, "node_modules/@pharmatech/sdk": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.16.tgz", - "integrity": "sha512-J8ycNdl+x7h7HH0PDJVXUzKyT9n0WfykHyg8gJTw37nziuQJcY5GVpo6gCmlG4bj78ucbUICQGJqc2zVE6Q8UQ==", + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.18.tgz", + "integrity": "sha512-AfMYi7/6d/Iq7H3hk9UQNx3vmw+hDOn4R5VeVe9cnJnbxtWjoXXTizCIMRZHdMuRhsuEeJ2rd1odOcqOLzQ4Yg==", "license": "MIT", "dependencies": { "axios": "^1.8.1" diff --git a/package.json b/package.json index 7ab554d..af5590f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@heroicons/react": "^2.2.0", "@next/third-parties": "^15.3.0", "cloudinary": "^2.6.0", - "@pharmatech/sdk": "^0.4.16", + "@pharmatech/sdk": "^0.4.18", "@radix-ui/react-slider": "^1.2.4", "@react-google-maps/api": "^2.20.6", "@types/google.maps": "^3.58.1", diff --git a/src/app/(shop)/page.tsx b/src/app/(shop)/page.tsx index d53798b..936ac22 100644 --- a/src/app/(shop)/page.tsx +++ b/src/app/(shop)/page.tsx @@ -9,7 +9,6 @@ import Banner3 from '@/lib/utils/images/banner_final.jpg'; import { toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import ProductDetailImg from '@/lib/utils/images/Antibioticos.png'; -import { ExtendedProduct } from '@/lib/types/ExtendedProduct'; import type { Category as SDKCategory, @@ -133,9 +132,7 @@ export default function Home() { Productos en Oferta Exclusiva
- +
{/* Sección recomendados */} @@ -145,9 +142,7 @@ export default function Home() { Productos Recomendados para ti
- +
)} diff --git a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx index 1293526..18fd5cc 100644 --- a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx +++ b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx @@ -18,7 +18,6 @@ import { ProductPresentation, ProductPaginationRequest, } from '@pharmatech/sdk'; -import type { ExtendedProduct } from '@/lib/types/ExtendedProduct'; import Loading from '@/app/loading'; import ProductNotFound from '@/components/Product/NotFound'; @@ -185,9 +184,7 @@ export default function ProductDetailPage() {

Productos de la marca {genericProduct.manufacturer.name}

- p as unknown as ExtendedProduct)} - /> + p)} />
); diff --git a/src/app/(shop)/search/page.tsx b/src/app/(shop)/search/page.tsx index 919444a..7854e85 100644 --- a/src/app/(shop)/search/page.tsx +++ b/src/app/(shop)/search/page.tsx @@ -8,7 +8,6 @@ import { api } from '@/lib/sdkConfig'; import Breadcrumb from '@/components/Breadcrumb'; import Loading from '@/app/loading'; import { ProductPaginationRequest, ProductPresentation } from '@pharmatech/sdk'; -import { ExtendedProduct } from '@/lib/types/ExtendedProduct'; interface CategoryOption { id: string; @@ -280,10 +279,7 @@ export default function SearchPage() {
{displayProducts.map((p) => ( - + ))}
diff --git a/src/components/Product/ProductCard.tsx b/src/components/Product/ProductCard.tsx index 7994542..f860740 100644 --- a/src/components/Product/ProductCard.tsx +++ b/src/components/Product/ProductCard.tsx @@ -2,12 +2,12 @@ import Image from 'next/image'; import Link from 'next/link'; -import type { ExtendedProduct } from '@/lib/types/ExtendedProduct'; import CardButton from '../CardButton'; import Badge from '../Badge'; +import { ProductPresentation } from '@pharmatech/sdk'; type Props = { - product: ExtendedProduct; + product: ProductPresentation; }; export default function ProductCard({ product }: Props) { diff --git a/src/components/Product/ProductCarousel.tsx b/src/components/Product/ProductCarousel.tsx index 86130ae..fa2bc6a 100644 --- a/src/components/Product/ProductCarousel.tsx +++ b/src/components/Product/ProductCarousel.tsx @@ -3,11 +3,11 @@ import { useRef } from 'react'; import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/solid'; import ProductCard from './ProductCard'; -import type { ExtendedProduct } from '@/lib/types/ExtendedProduct'; +import { ProductPresentation } from '@pharmatech/sdk'; interface ProductSliderProps { title?: string; - products: ExtendedProduct[]; + products: ProductPresentation[]; } export default function ProductSlider({ title, products }: ProductSliderProps) { diff --git a/src/lib/types/ExtendedProduct.ts b/src/lib/types/ExtendedProduct.ts deleted file mode 100644 index 499c028..0000000 --- a/src/lib/types/ExtendedProduct.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { - ProductPresentationDetailResponse, - PromoResponse, - Product, -} from '@pharmatech/sdk'; - -export type ExtendedProduct = ProductPresentationDetailResponse & { - product: Product; - promo?: PromoResponse | PromoResponse[] | null; -}; From ceec4d3203bc27e98b62ebed5538daf3f203c626 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sat, 17 May 2025 12:05:43 -0400 Subject: [PATCH 11/50] Fix for only one promo instead of promo array --- src/components/Product/ProductCard.tsx | 39 +++++++++++--------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/components/Product/ProductCard.tsx b/src/components/Product/ProductCard.tsx index f860740..7dd3e75 100644 --- a/src/components/Product/ProductCard.tsx +++ b/src/components/Product/ProductCard.tsx @@ -13,30 +13,21 @@ type Props = { export default function ProductCard({ product }: Props) { const stock = product.stock ?? 0; if (stock < 0) return null; - + let hasDiscount = false; const name = `${product.product.name} ${product.presentation.name}`; const imageUrl = product.product.images?.[0]?.url || '/placeholder.svg'; const price = product.price; - const now = new Date(); - - const promosArray = Array.isArray(product.promo) - ? product.promo - : product.promo - ? [product.promo] - : []; - - const activePromos = promosArray.filter((promo) => { - const start = new Date(promo.startAt); - const end = new Date(promo.expiredAt); - return start <= now && now < end; - }); - - const totalDiscount = activePromos.reduce( - (sum, promo) => sum + (promo.discount || 0), - 0, - ); - const hasDiscount = totalDiscount > 0; - const finalPrice = hasDiscount ? price * (1 - totalDiscount / 100) : price; + let finalPrice = price; + if (product.promo) { + const now = new Date(); + const start = new Date(product.promo.startAt); + const end = new Date(product.promo.expiredAt); + const activePromo = start <= now && now < end; + hasDiscount = product.promo.discount > 0 && activePromo; + finalPrice = hasDiscount + ? price * (1 - product.promo.discount / 100) + : price; + } const firstCategory = product.product.categories?.[0]; const href = `/product/${product.product.id}/presentation/${product.presentation.id}`; @@ -90,7 +81,9 @@ export default function ProductCard({ product }: Props) { size="small" borderRadius="square" > - -{totalDiscount}% + + -{product.promo?.discount}% +
@@ -110,7 +103,7 @@ export default function ProductCard({ product }: Props) { productPresentationId: product.id, name: product.product.name, price, - discount: hasDiscount ? totalDiscount : undefined, + discount: hasDiscount ? product.promo?.discount : undefined, image: imageUrl, stock, }} From 76b99ad0488000b765717159f01cdeb59e12f010 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sat, 17 May 2025 13:15:01 -0400 Subject: [PATCH 12/50] Install microsoft fetch event and add event source for notifications --- package-lock.json | 7 ++++++ package.json | 3 ++- src/components/Navbar.tsx | 51 +++++++++++++++++++++++++++++++++++++-- src/lib/sdkConfig.ts | 4 +++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a4a795a..17099ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@headlessui/react": "^2.2.0", "@heroicons/react": "^2.2.0", + "@microsoft/fetch-event-source": "^2.0.1", "@next/third-parties": "^15.3.0", "@pharmatech/sdk": "^0.4.18", "@radix-ui/react-slider": "^1.2.4", @@ -1718,6 +1719,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@microsoft/fetch-event-source": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", + "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==", + "license": "MIT" + }, "node_modules/@next/env": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.0.tgz", diff --git a/package.json b/package.json index af5590f..075819c 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,13 @@ "dependencies": { "@headlessui/react": "^2.2.0", "@heroicons/react": "^2.2.0", + "@microsoft/fetch-event-source": "^2.0.1", "@next/third-parties": "^15.3.0", - "cloudinary": "^2.6.0", "@pharmatech/sdk": "^0.4.18", "@radix-ui/react-slider": "^1.2.4", "@react-google-maps/api": "^2.20.6", "@types/google.maps": "^3.58.1", + "cloudinary": "^2.6.0", "jwt-decode": "^4.0.0", "lucide-react": "^0.477.0", "next": "^15.3.0", diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index d7ee84e..5ad6cc4 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -8,6 +8,7 @@ import { UserCircleIcon, BellIcon, } from '@heroicons/react/24/outline'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; import Avatar from '@/components/Avatar'; import SearchBar from '@/components/SearchBar'; import { useRouter } from 'next/navigation'; @@ -16,7 +17,7 @@ import { Colors } from '../styles/styles'; import Button from '@/components/Button'; import { useCart } from '@/context/CartContext'; import { useAuth } from '@/context/AuthContext'; -import { api } from '@/lib/sdkConfig'; +import { api, API_URL } from '@/lib/sdkConfig'; import { CategoryResponse, Pagination } from '@pharmatech/sdk'; import CartOverlay from './Cart/CartOverlay'; import NotificationList from '@/components/User/NotificationList'; @@ -45,6 +46,7 @@ export default function NavBar({ onCartClick }: NavBarProps) { const [isLoggedIn, setIsLoggedIn] = useState(false); const [userData, setUserData] = useState(null); const [isCartOpen, setIsCartOpen] = useState(false); + const [notificationCount, setNotificationCount] = useState(0); useEffect(() => { api.category @@ -91,6 +93,45 @@ export default function NavBar({ onCartClick }: NavBarProps) { return () => document.removeEventListener('mousedown', handleClickOutside); }, []); + useEffect(() => { + const controller = new AbortController(); + const fetchData = async () => { + await fetchEventSource(`${API_URL}/notification/stream`, { + headers: { + Authorization: `Bearer ${token}`, + }, + async onopen(res) { + if (res.ok && res.status === 200) { + console.log('Connection made ', res); + } else if ( + res.status >= 400 && + res.status < 500 && + res.status !== 429 + ) { + console.log('Client side error ', res); + } + }, + onmessage(event) { + console.log('New message from server', event); + setNotificationCount((prev) => prev + 1); + }, + onclose() { + console.log('Connection closed by the server'); + }, + onerror(err) { + console.log('There was an error from server', err); + }, + }); + }; + if (token) { + fetchData(); + } + return () => { + controller.abort(); + console.log('Connection aborted'); + }; + }, [token]); + const handleSearch = (query: string, category: string) => { console.log('Buscando:', query, 'en', category); }; @@ -144,8 +185,14 @@ export default function NavBar({ onCartClick }: NavBarProps) {
setIsNotificationsOpen((prev) => !prev)} + onClick={() => { + setNotificationCount(0); + setIsNotificationsOpen((prev) => !prev); + }} > +
+ {notificationCount} +
{isNotificationsOpen && ( diff --git a/src/lib/sdkConfig.ts b/src/lib/sdkConfig.ts index ae74496..51d8cf4 100644 --- a/src/lib/sdkConfig.ts +++ b/src/lib/sdkConfig.ts @@ -2,4 +2,8 @@ import { PharmaTech } from '@pharmatech/sdk'; const devModeFlag = process.env.NEXT_PUBLIC_DEV_MODE === 'true'; +const BASE_URL = 'https://api-d8h5.onrender.com'; +const DEV_URL = 'https://api-dev-8jfx.onrender.com'; +export const API_URL = devModeFlag ? DEV_URL : BASE_URL; + export const api = PharmaTech.getInstance(devModeFlag); From ff8481c6ba8888e9e2bec5b39ebb60471dd8581f Mon Sep 17 00:00:00 2001 From: Gustavo <1001.29587789.ucla@gmail.com> Date: Sun, 18 May 2025 00:18:56 -0400 Subject: [PATCH 13/50] updatePasswordSchema: add newPassword rules --- src/lib/validations/updatePasswordSchema.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/lib/validations/updatePasswordSchema.ts b/src/lib/validations/updatePasswordSchema.ts index 0ae90b8..e407c29 100644 --- a/src/lib/validations/updatePasswordSchema.ts +++ b/src/lib/validations/updatePasswordSchema.ts @@ -2,18 +2,21 @@ import { z } from 'zod'; export const updatePasswordSchema = z .object({ - password: z - .string() - .nonempty('La contraseña es obligatoria') - .min(8, 'La contraseña debe tener al menos 8 caracteres'), + password: z.string().nonempty('La contraseña actual es obligatoria'), + newPassword: z .string() .nonempty('La contraseña es obligatoria') - .min(8, 'La contraseña debe tener al menos 8 caracteres'), - confirmPassword: z - .string() - .nonempty('La confirmación es obligatoria') - .min(8, 'La contraseña debe tener al menos 8 caracteres'), + .min(8, 'La contraseña debe tener al menos 8 caracteres') + .regex(/[A-Z]/, 'Debe tener al menos una letra mayúscula') + .regex(/[a-z]/, 'Debe tener al menos una letra minúscula') + .regex(/\d/, 'Debe tener al menos un número') + .regex( + /[!@#$%^&*]/, + 'Debe tener al menos un símbolo especial (!@#$%^&*)', + ), + + confirmPassword: z.string().nonempty('La confirmación es obligatoria'), }) .refine((data) => data.newPassword === data.confirmPassword, { message: 'Las contraseñas no coinciden', From 3eb8996d31694c042f5e61746821b5575db16b36 Mon Sep 17 00:00:00 2001 From: Gustavo <1001.29587789.ucla@gmail.com> Date: Sun, 18 May 2025 00:33:05 -0400 Subject: [PATCH 14/50] recoverPasswordSchema: add newPassword rules --- src/lib/validations/recoverPasswordSchema.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/lib/validations/recoverPasswordSchema.ts b/src/lib/validations/recoverPasswordSchema.ts index 7bfe192..b58c22a 100644 --- a/src/lib/validations/recoverPasswordSchema.ts +++ b/src/lib/validations/recoverPasswordSchema.ts @@ -10,11 +10,16 @@ export const resetPasswordSchema = z newPassword: z .string() .nonempty('La contraseña es obligatoria') - .min(8, 'La contraseña debe tener al menos 8 caracteres'), - confirmPassword: z - .string() - .nonempty('La confirmación es obligatoria') - .min(8, 'La contraseña debe tener al menos 8 caracteres'), + .min(8, 'La contraseña debe tener al menos 8 caracteres') + .regex(/[A-Z]/, 'Debe tener al menos una letra mayúscula') + .regex(/[a-z]/, 'Debe tener al menos una letra minúscula') + .regex(/\d/, 'Debe tener al menos un número') + .regex( + /[!@#$%^&*]/, + 'Debe tener al menos un símbolo especial (!@#$%^&*)', + ), + + confirmPassword: z.string().nonempty('La confirmación es obligatoria'), }) .refine((data) => data.newPassword === data.confirmPassword, { message: 'Las contraseñas no coinciden', From 36ef03ab761a29c54bd7d1c65fe58ec998b3efde Mon Sep 17 00:00:00 2001 From: Gustavo <1001.29587789.ucla@gmail.com> Date: Sun, 18 May 2025 01:05:10 -0400 Subject: [PATCH 15/50] checkoutSchema: phoneNumber no longer requires '+' --- src/lib/validations/checkoutPaymentProcessSchema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/validations/checkoutPaymentProcessSchema.ts b/src/lib/validations/checkoutPaymentProcessSchema.ts index de90acf..86c55ac 100644 --- a/src/lib/validations/checkoutPaymentProcessSchema.ts +++ b/src/lib/validations/checkoutPaymentProcessSchema.ts @@ -33,7 +33,7 @@ export const checkoutPaymentProcessSchema = z.object({ .string() .trim() // Elimina espacios en blanco al inicio y al final .refine( - (value) => /^\+\d{8,15}$/.test(value), // El teléfono debe iniciar con + y tener entre 8 y 15 dígitos - 'El teléfono debe iniciar con + y tener entre 8 y 15 dígitos', + (value) => /^\d{8,15}$/.test(value), + 'El teléfono debe tener entre 8 y 15 dígitos numéricos', ), }); From 0d92e988aa803f62f508dca3217e473819dcad54 Mon Sep 17 00:00:00 2001 From: Gustavo <1001.29587789.ucla@gmail.com> Date: Sun, 18 May 2025 01:07:46 -0400 Subject: [PATCH 16/50] registerSchema: phoneNumber no longer requires '+' --- src/lib/validations/registerSchema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/validations/registerSchema.ts b/src/lib/validations/registerSchema.ts index 8350041..5d0b4a6 100644 --- a/src/lib/validations/registerSchema.ts +++ b/src/lib/validations/registerSchema.ts @@ -30,8 +30,8 @@ const baseSchema = z.object({ .transform((value) => (value?.trim() === '' ? null : value)) .nullable() .refine( - (value) => value === null || /^\+\d{8,15}$/.test(value), - 'El teléfono debe iniciar con + y tener entre 8 y 15 dígitos', + (value) => value === null || /^\d{8,15}$/.test(value), + 'El teléfono debe tener entre 8 y 15 dígitos numéricos', ), fechaNacimiento: z From 5a30acc6b702045e9376f4dbe5ac612ef776915a Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Sun, 18 May 2025 08:13:45 -0400 Subject: [PATCH 17/50] fix in style badge color, mobile and catching event for counter --- src/components/Navbar.tsx | 85 ++++++++++++++---------- src/components/User/NotificationBell.tsx | 43 ++++++++++++ 2 files changed, 94 insertions(+), 34 deletions(-) create mode 100644 src/components/User/NotificationBell.tsx diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 5ad6cc4..a894071 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -3,11 +3,7 @@ import React, { useEffect, useRef, useState } from 'react'; import Link from 'next/link'; import Image from 'next/image'; -import { - ShoppingCartIcon, - UserCircleIcon, - BellIcon, -} from '@heroicons/react/24/outline'; +import { ShoppingCartIcon, UserCircleIcon } from '@heroicons/react/24/outline'; import { fetchEventSource } from '@microsoft/fetch-event-source'; import Avatar from '@/components/Avatar'; import SearchBar from '@/components/SearchBar'; @@ -20,7 +16,7 @@ import { useAuth } from '@/context/AuthContext'; import { api, API_URL } from '@/lib/sdkConfig'; import { CategoryResponse, Pagination } from '@pharmatech/sdk'; import CartOverlay from './Cart/CartOverlay'; -import NotificationList from '@/components/User/NotificationList'; +import NotificationBell from '@/components/User/NotificationBell'; interface UserProfile { firstName: string; @@ -112,8 +108,20 @@ export default function NavBar({ onCartClick }: NavBarProps) { } }, onmessage(event) { - console.log('New message from server', event); - setNotificationCount((prev) => prev + 1); + try { + if (!event.data) return; + + const parsed = JSON.parse(event.data); + + if (parsed?.message && parsed?.order?.id) { + console.log('New message from server', event); + setNotificationCount((prev) => prev + 1); + } else { + console.log('Ignored message from server', parsed); + } + } catch (err) { + console.log('Error parsing message from server', err); + } }, onclose() { console.log('Connection closed by the server'); @@ -182,25 +190,16 @@ export default function NavBar({ onCartClick }: NavBarProps) {
{/* Notificaciones */} -
-
{ - setNotificationCount(0); - setIsNotificationsOpen((prev) => !prev); - }} - > -
- {notificationCount} -
- -
- {isNotificationsOpen && ( -
- -
- )} -
+ { + setNotificationCount(0); + setIsNotificationsOpen((prev) => !prev); + }} + refProp={notificationsRef} + /> {/* Usuario */} {isLoggedIn && userData ? ( @@ -228,7 +227,8 @@ export default function NavBar({ onCartClick }: NavBarProps) { {/* Versión Mobile */} @@ -237,12 +232,12 @@ export default function NavBar({ onCartClick }: NavBarProps) { withDropdown={true} onProfileClick={() => router.push('/user')} /> - ) : ( + ) : showLogin ? ( - )} + ) : null} {/* Columna centro: logo */} diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 8aa8ac7..64650f5 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -23,6 +23,7 @@ interface AuthContextType { user: JwtPayload | null; login: (token: string, remember: boolean) => void; logout: () => void; + isLoading: boolean; } const AuthContext = createContext({ @@ -30,6 +31,7 @@ const AuthContext = createContext({ user: null, login: () => {}, logout: () => {}, + isLoading: true, }); const decodeToken = (rawToken: string): JwtPayload | null => { @@ -52,6 +54,8 @@ const decodeToken = (rawToken: string): JwtPayload | null => { export const AuthProvider = ({ children }: { children: ReactNode }) => { const [token, setToken] = useState(null); const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const router = useRouter(); useEffect(() => { @@ -64,6 +68,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const decoded = decodeToken(storedToken); setUser(decoded); } + setIsLoading(false); }, []); useEffect(() => { @@ -104,7 +109,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { }; return ( - + {children} ); diff --git a/src/lib/utils/constants/DateUtils.ts b/src/lib/utils/constants/DateUtils.ts index 1353abc..16818b0 100644 --- a/src/lib/utils/constants/DateUtils.ts +++ b/src/lib/utils/constants/DateUtils.ts @@ -18,6 +18,7 @@ export const WEEK_DAYS = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb']; export const formatTimeAgo = (date: string): string => { const created = new Date(date); const diff = Math.floor((Date.now() - created.getTime()) / 1000); + if (diff < 0) return 'Hace unos segundos'; if (diff < 60) return `Hace ${diff} segundos`; if (diff < 3600) return `Hace ${Math.floor(diff / 60)} minutos`; if (diff < 86400) return `Hace ${Math.floor(diff / 3600)} horas`; From 9d94ad94b4d54904c82475ba96e2ad32e15172a4 Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Sun, 18 May 2025 10:04:41 -0400 Subject: [PATCH 19/50] fix in navigation state log out in navbar --- .../presentation/[presentationId]/page.tsx | 3 +++ src/context/AuthContext.tsx | 13 +++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx index 18fd5cc..2295ce8 100644 --- a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx +++ b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx @@ -20,11 +20,13 @@ import { } from '@pharmatech/sdk'; import Loading from '@/app/loading'; import ProductNotFound from '@/components/Product/NotFound'; +import { useAuth } from '@/context/AuthContext'; export default function ProductDetailPage() { const router = useRouter(); const params = useParams(); const searchParams = useSearchParams(); + const { isLoading } = useAuth(); // Detect if we came from a filtered search const queryString = searchParams?.toString() || ''; @@ -131,6 +133,7 @@ export default function ProductDetailPage() { if (found) router.push(`/product/${productId}/presentation/${found.id}`); }; + if (isLoading) return ; return (
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 64650f5..e791c1c 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -37,13 +37,10 @@ const AuthContext = createContext({ const decodeToken = (rawToken: string): JwtPayload | null => { try { const decoded = jwtDecode(rawToken); - - // (Opcional) Verificar expiración if (decoded.exp && decoded.exp * 1000 < Date.now()) { console.warn('Token expirado'); return null; } - return decoded; } catch (error) { console.error('Error decoding token:', error); @@ -60,8 +57,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { useEffect(() => { const storedToken = - sessionStorage.getItem('pharmatechToken') || - localStorage.getItem('pharmatechToken'); + localStorage.getItem('pharmatechToken') || + sessionStorage.getItem('pharmatechToken'); if (storedToken) { setToken(storedToken); @@ -89,9 +86,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { }, []); const login = (newToken: string, remember: boolean) => { - sessionStorage.setItem('pharmatechToken', newToken); - if (remember) { - localStorage.setItem('pharmatechToken', newToken); + localStorage.setItem('pharmatechToken', newToken); + if (!remember) { + sessionStorage.setItem('pharmatechToken', newToken); } setToken(newToken); From 52b391410e3397933a1592c5f878362881777496 Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Sun, 18 May 2025 13:54:01 -0400 Subject: [PATCH 20/50] changes requested in loading state and time in notification --- .../presentation/[presentationId]/page.tsx | 9 +- src/components/Navbar.tsx | 110 +++++++++++------- src/components/User/NotificationBell.tsx | 5 +- src/components/User/NotificationList.tsx | 42 ++----- src/lib/utils/constants/DateUtils.ts | 23 +++- 5 files changed, 101 insertions(+), 88 deletions(-) diff --git a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx index 2295ce8..766afdf 100644 --- a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx +++ b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx @@ -18,15 +18,12 @@ import { ProductPresentation, ProductPaginationRequest, } from '@pharmatech/sdk'; -import Loading from '@/app/loading'; import ProductNotFound from '@/components/Product/NotFound'; -import { useAuth } from '@/context/AuthContext'; export default function ProductDetailPage() { const router = useRouter(); const params = useParams(); const searchParams = useSearchParams(); - const { isLoading } = useAuth(); // Detect if we came from a filtered search const queryString = searchParams?.toString() || ''; @@ -44,7 +41,6 @@ export default function ProductDetailPage() { const [presentationList, setPresentationList] = useState< ProductPresentationResponse[] >([]); - const [loading, setLoading] = useState(true); // 1) Load presentation detail useEffect(() => { @@ -104,11 +100,9 @@ export default function ProductDetailPage() { api.product .getProducts(req) .then((res) => setProducts(res.results)) - .catch((err) => console.error(err)) - .finally(() => setLoading(false)); + .catch((err) => console.error(err)); }, [genericProduct]); - if (loading) return ; if (!presentation || !genericProduct) return ; // Breadcrumb con acción de "volver" si es búsqueda personalizada @@ -133,7 +127,6 @@ export default function ProductDetailPage() { if (found) router.push(`/product/${productId}/presentation/${found.id}`); }; - if (isLoading) return ; return (
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index a43b6ea..314cd5e 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -14,7 +14,11 @@ import Button from '@/components/Button'; import { useCart } from '@/context/CartContext'; import { useAuth } from '@/context/AuthContext'; import { api, API_URL } from '@/lib/sdkConfig'; -import { CategoryResponse, Pagination } from '@pharmatech/sdk'; +import { + CategoryResponse, + Pagination, + NotificationResponse, +} from '@pharmatech/sdk'; import CartOverlay from './Cart/CartOverlay'; import NotificationBell from '@/components/User/NotificationBell'; @@ -41,9 +45,12 @@ export default function NavBar({ onCartClick }: NavBarProps) { const totalCount = cartItems.reduce((acc, item) => acc + item.quantity, 0); const [isLoggedIn, setIsLoggedIn] = useState(false); const [userData, setUserData] = useState(null); - const [isCartOpen, setIsCartOpen] = useState(false); + const [isCartOpen, setIsCartOpen] = useState(false); const [notificationCount, setNotificationCount] = useState(0); const [showLogin, setShowLogin] = useState(false); + const [notifications, setNotifications] = useState( + [], + ); useEffect(() => { api.category @@ -51,7 +58,7 @@ export default function NavBar({ onCartClick }: NavBarProps) { .then((resp: Pagination) => { if (resp?.results) setCategories(resp.results); }) - .catch((err: unknown) => { + .catch((err) => { console.error('Error al cargar categorías:', err); }); }, []); @@ -66,7 +73,6 @@ export default function NavBar({ onCartClick }: NavBarProps) { } catch (error) { console.error('Error al obtener perfil:', error); setUserData(null); - } finally { } })(); } else { @@ -79,61 +85,63 @@ export default function NavBar({ onCartClick }: NavBarProps) { const timeout = setTimeout(() => { setShowLogin(true); }, 1000); - return () => clearTimeout(timeout); }, []); - // Cerrar dropdown si se hace click fuera - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - notificationsRef.current && - !notificationsRef.current.contains(event.target as Node) - ) { - setIsNotificationsOpen(false); - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - + // Fetch notifications stream useEffect(() => { const controller = new AbortController(); + const fetchData = async () => { await fetchEventSource(`${API_URL}/notification/stream`, { headers: { Authorization: `Bearer ${token}`, }, + signal: controller.signal, async onopen(res) { if (res.ok && res.status === 200) { - console.log('Connection made ', res); + console.log('Connection made', res); } else if ( res.status >= 400 && res.status < 500 && res.status !== 429 ) { - console.log('Client side error ', res); + console.log('Client side error', res); } }, onmessage(event) { console.log('New message from server', event); setNotificationCount((prev) => prev + 1); }, - onerror(err) { console.log('There was an error from server', err); }, }); }; + if (token) { fetchData(); } + return () => { controller.abort(); console.log('Connection aborted'); }; }, [token]); + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + notificationsRef.current && + !notificationsRef.current.contains(event.target as Node) + ) { + setIsNotificationsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + const handleSearch = (query: string, category: string) => { console.log('Buscando:', query, 'en', category); }; @@ -141,14 +149,48 @@ export default function NavBar({ onCartClick }: NavBarProps) { const handleLoginClick = () => { router.push('/login'); }; + + const handleNotificationToggle = async () => { + const willOpen = !isNotificationsOpen; + + if (willOpen && token) { + try { + const res = await api.notification.getNotifications(token); + if (Array.isArray(res)) { + setNotifications(res); + + const unread = res.filter((n) => !n.isRead); + setNotificationCount(unread.length); + // Mark notifications as read + if (unread.length > 0) { + await Promise.all( + unread.map((notif) => + api.notification.markAsRead(notif.order.id, token), + ), + ); + setNotifications((prev) => + prev.map((n) => + unread.some((u) => u.id === n.id) ? { ...n, isRead: true } : n, + ), + ); + setNotificationCount(0); + } + } + } catch (err) { + console.error('Error fetching notifications:', err); + } + } + + setIsNotificationsOpen(willOpen); + }; + if (isLoading) return null; return ( <> - {/* Cart Overlay */} setIsCartOpen(false)} /> - {/* Versión Desktop */} + {/* Desktop Nav */} - {/* Versión Mobile */} + {/* Mobile Nav */}
@@ -246,16 +247,16 @@ export default function RegisterForm() {

- {errors.telefono} + {errors.phoneNumber}

@@ -276,9 +277,9 @@ export default function RegisterForm() {

- {errors.fechaNacimiento} + {errors.birthDate}

@@ -288,19 +289,19 @@ export default function RegisterForm() {
handleGenderClick('hombre')} /> handleGenderClick('mujer')} />

- {errors.genero} + {errors.gender}

diff --git a/src/components/User/UserProfileForm.tsx b/src/components/User/UserProfileForm.tsx index 453b16c..ce856ff 100644 --- a/src/components/User/UserProfileForm.tsx +++ b/src/components/User/UserProfileForm.tsx @@ -120,21 +120,21 @@ export default function EditForm({ const genderText = gender === UserGender.FEMALE ? 'mujer' : 'hombre'; const result = editProfileSchema.safeParse({ - nombre: firstName, - apellido: lastName, - telefono: phoneNumber, - fechaNacimiento: birthDate, - genero: genderText, + firstName: firstName, + lastName: lastName, + phoneNumber: phoneNumber, + birthDate: birthDate, + gender: genderText, }); if (!result.success) { const { fieldErrors } = result.error.flatten(); setErrors({ - firstName: fieldErrors.nombre?.[0] ?? '', - lastName: fieldErrors.apellido?.[0] ?? '', - phoneNumber: fieldErrors.telefono?.[0] ?? '', - birthDate: fieldErrors.fechaNacimiento?.[0] ?? '', - gender: fieldErrors.genero?.[0] ?? '', + firstName: fieldErrors.firstName?.[0] ?? '', + lastName: fieldErrors.lastName?.[0] ?? '', + phoneNumber: fieldErrors.phoneNumber?.[0] ?? '', + birthDate: fieldErrors.birthDate?.[0] ?? '', + gender: fieldErrors.gender?.[0] ?? '', }); return; } diff --git a/src/lib/validations/registerSchema.ts b/src/lib/validations/registerSchema.ts index 2d20a2d..f4c87b0 100644 --- a/src/lib/validations/registerSchema.ts +++ b/src/lib/validations/registerSchema.ts @@ -62,9 +62,10 @@ const baseSchema = z.object({ gender: z .string() - .nonempty('Selecciona un género') + .transform((val) => (val?.trim() === '' ? null : val)) + .nullable() .refine( - (value) => value === 'hombre' || value === 'mujer', + (value) => value === null || value === 'hombre' || value === 'mujer', 'Selecciona un género válido', ), From 501f940a6b9577b2a9aa239c7842c3047ef52866 Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Sun, 18 May 2025 16:26:50 -0400 Subject: [PATCH 28/50] counter validation, stream timeout --- src/components/Navbar.tsx | 58 ++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 6f94797..306c66e 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -91,46 +91,48 @@ export default function NavBar({ onCartClick }: NavBarProps) { // Fetch notifications stream useEffect(() => { - const controller = new AbortController(); + if (!token) return; - const fetchData = async () => { - await fetchEventSource(`${API_URL}/notification/stream`, { - headers: { - Authorization: `Bearer ${token}`, - }, - signal: controller.signal, + let aborted = false; + let retryId: NodeJS.Timeout; + + const connect = () => { + fetchEventSource(`${API_URL}/notification/stream`, { + headers: { Authorization: `Bearer ${token}` }, + openWhenHidden: true, async onopen(res) { - if (res.ok && res.status === 200) { - console.log('Connection made', res); - } else if ( - res.status >= 400 && - res.status < 500 && - res.status !== 429 - ) { - console.log('Client side error', res); - } + if (res.ok) console.log('SSE abierta'); }, - onmessage(event) { - if (event.event == 'notification') { - setNotificationCount((prev) => prev + 1); - } + onmessage(ev) { + if (!ev.data) return; + + try { + const d = JSON.parse(ev.data); + if (d.type !== 'notification') return; + + setNotifications((prev) => { + const exists = prev.some((n) => n.id === d.payload.id); + if (exists) return prev; + + setNotificationCount((c) => c + 1); + return [d.payload, ...prev]; + }); + } catch {} }, onclose() { - console.log('Connection closed by the server'); + if (!aborted) retryId = setTimeout(connect, 5000); }, - onerror(err) { - console.log('There was an error from server', err); + onerror() { + if (!aborted) retryId = setTimeout(connect, 5000); }, }); }; - if (token) { - fetchData(); - } + connect(); return () => { - controller.abort(); - console.log('Connection aborted'); + aborted = true; + clearTimeout(retryId); }; }, [token]); From f5be8843f5e1f42d513f8a228cd328cb08ab38b6 Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Tue, 20 May 2025 21:41:52 -0400 Subject: [PATCH 29/50] general fixes in badge, order table, cardbutton --- src/components/Badge.tsx | 19 ++++++++++++++----- src/components/Product/ProductCard.tsx | 4 ++-- src/components/User/Order/UserOrdertable.tsx | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 86725c1..2f45f26 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -2,21 +2,26 @@ import React from 'react'; import '../styles/globals.css'; import { Colors } from '../styles/styles'; + export interface BadgeProps { variant: 'filled' | 'outlined' | 'text'; color: 'primary' | 'tertiary' | 'warning' | 'danger' | 'success' | 'info'; size: 'small' | 'medium' | 'large'; borderRadius?: 'rounded' | 'square'; + className?: string; children: React.ReactNode; } + const Badge: React.FC = ({ variant, color, size, borderRadius = 'rounded', + className, children, }) => { - const baseStyle = 'font-poppins px-4 py-2'; + const baseStyle = + 'inline-flex items-center justify-center font-poppins select-none px-4 py-2 whitespace-nowrap w-fit'; const sizeStyle = size === 'small' ? 'text-sm mt-2' @@ -37,7 +42,6 @@ const Badge: React.FC = ({ color: Colors.textWhite, borderColor: Colors.secondaryLight, }, - warning: { backgroundColor: Colors.semanticWarning, color: Colors.textWhite, @@ -59,6 +63,7 @@ const Badge: React.FC = ({ borderColor: Colors.semanticInfo, }, }; + const outlinedClasses: Record = { primary: { color: Colors.primary, @@ -85,6 +90,7 @@ const Badge: React.FC = ({ border: `1px solid ${Colors.semanticInfo}`, }, }; + const textClasses: Record = { primary: { color: Colors.primary }, warning: { color: Colors.semanticWarning }, @@ -92,18 +98,21 @@ const Badge: React.FC = ({ success: { color: Colors.semanticSuccess }, info: { color: Colors.semanticInfo }, }; - const variantStyle = { + + const styleByVariant = { filled: colorClasses[color], outlined: outlinedClasses[color], text: textClasses[color], }; + return ( {children} ); }; + export default Badge; diff --git a/src/components/Product/ProductCard.tsx b/src/components/Product/ProductCard.tsx index 7dd3e75..7b8ab79 100644 --- a/src/components/Product/ProductCard.tsx +++ b/src/components/Product/ProductCard.tsx @@ -67,7 +67,7 @@ export default function ProductCard({ product }: Props) {

-
+
{hasDiscount ? ( <> @@ -97,7 +97,7 @@ export default function ProductCard({ product }: Props) { )}
-
+
{badge.label} From db43e42acacd71700d598e6132b67e25f7aa8743 Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Tue, 20 May 2025 22:06:45 -0400 Subject: [PATCH 30/50] fix in scroll --- src/components/Navbar.tsx | 86 ++++++++++-------------- src/components/User/NotificationBell.tsx | 10 +-- src/components/User/NotificationList.tsx | 2 +- 3 files changed, 41 insertions(+), 57 deletions(-) diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 306c66e..300cbc8 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import Link from 'next/link'; import Image from 'next/image'; import { ShoppingCartIcon, UserCircleIcon } from '@heroicons/react/24/outline'; @@ -39,7 +39,6 @@ export default function NavBar({ onCartClick }: NavBarProps) { const router = useRouter(); const [categories, setCategories] = useState([]); const [isNotificationsOpen, setIsNotificationsOpen] = useState(false); - const notificationsRef = useRef(null); const { cartItems } = useCart(); const { token, user, isLoading } = useAuth(); @@ -53,6 +52,40 @@ export default function NavBar({ onCartClick }: NavBarProps) { [], ); + const handleNotificationToggle = async () => { + const willOpen = !isNotificationsOpen; + + if (willOpen && token) { + try { + const res = await api.notification.getNotifications(token); + if (Array.isArray(res)) { + setNotifications(res); + + const unread = res.filter((n) => !n.isRead); + setNotificationCount(unread.length); + + if (unread.length > 0) { + await Promise.all( + unread.map((notif) => + api.notification.markAsRead(notif.order.id, token), + ), + ); + setNotifications((prev) => + prev.map((n) => + unread.some((u) => u.id === n.id) ? { ...n, isRead: true } : n, + ), + ); + setNotificationCount(0); + } + } + } catch (err) { + console.error('Error fetching notifications:', err); + } + } + + setIsNotificationsOpen(willOpen); + }; + useEffect(() => { api.category .findAll({ page: 1, limit: 20 }) @@ -136,19 +169,6 @@ export default function NavBar({ onCartClick }: NavBarProps) { }; }, [token]); - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - notificationsRef.current && - !notificationsRef.current.contains(event.target as Node) - ) { - setIsNotificationsOpen(false); - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - const handleSearch = (query: string, category: string) => { console.log('Buscando:', query, 'en', category); }; @@ -157,40 +177,6 @@ export default function NavBar({ onCartClick }: NavBarProps) { router.push('/login'); }; - const handleNotificationToggle = async () => { - const willOpen = !isNotificationsOpen; - - if (willOpen && token) { - try { - const res = await api.notification.getNotifications(token); - if (Array.isArray(res)) { - setNotifications(res); - - const unread = res.filter((n) => !n.isRead); - setNotificationCount(unread.length); - // Mark notifications as read - if (unread.length > 0) { - await Promise.all( - unread.map((notif) => - api.notification.markAsRead(notif.order.id, token), - ), - ); - setNotifications((prev) => - prev.map((n) => - unread.some((u) => u.id === n.id) ? { ...n, isRead: true } : n, - ), - ); - setNotificationCount(0); - } - } - } catch (err) { - console.error('Error fetching notifications:', err); - } - } - - setIsNotificationsOpen(willOpen); - }; - if (isLoading) return null; return ( @@ -237,7 +223,6 @@ export default function NavBar({ onCartClick }: NavBarProps) { notificationCount={notificationCount} isOpen={isNotificationsOpen} onToggle={handleNotificationToggle} - refProp={notificationsRef} notifications={notifications} /> @@ -298,7 +283,6 @@ export default function NavBar({ onCartClick }: NavBarProps) { notificationCount={notificationCount} isOpen={isNotificationsOpen} onToggle={handleNotificationToggle} - refProp={notificationsRef} notifications={notifications} />
diff --git a/src/components/User/NotificationBell.tsx b/src/components/User/NotificationBell.tsx index a8c6bb3..29c7571 100644 --- a/src/components/User/NotificationBell.tsx +++ b/src/components/User/NotificationBell.tsx @@ -1,7 +1,6 @@ 'use client'; import { BellIcon } from '@heroicons/react/24/outline'; -import { RefObject } from 'react'; import NotificationList from '@/components/User/NotificationList'; import type { NotificationResponse } from '@pharmatech/sdk'; @@ -10,7 +9,6 @@ interface Props { notificationCount: number; isOpen: boolean; onToggle: () => void; - refProp: RefObject; notifications: NotificationResponse[]; } @@ -19,19 +17,21 @@ export default function NotificationBell({ notificationCount, isOpen, onToggle, - refProp, notifications, }: Props) { return ( -
+
{notificationCount > 0 && ( {notificationCount} )} + + {isOpen && (
e.stopPropagation()} + onPointerDownCapture={(e) => e.stopPropagation()} >

Notificaciones From 50bae637eac14d8a8a63d96f07c69385b9673659 Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Tue, 20 May 2025 23:17:22 -0400 Subject: [PATCH 31/50] discount amount fix --- package-lock.json | 8 +-- package.json | 2 +- src/components/User/Order/UserOrderDetail.tsx | 71 ++++++++++++------- 3 files changed, 52 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 17099ae..fc8fbde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@heroicons/react": "^2.2.0", "@microsoft/fetch-event-source": "^2.0.1", "@next/third-parties": "^15.3.0", - "@pharmatech/sdk": "^0.4.18", + "@pharmatech/sdk": "^0.4.19", "@radix-ui/react-slider": "^1.2.4", "@react-google-maps/api": "^2.20.6", "@types/google.maps": "^3.58.1", @@ -1928,9 +1928,9 @@ } }, "node_modules/@pharmatech/sdk": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.18.tgz", - "integrity": "sha512-AfMYi7/6d/Iq7H3hk9UQNx3vmw+hDOn4R5VeVe9cnJnbxtWjoXXTizCIMRZHdMuRhsuEeJ2rd1odOcqOLzQ4Yg==", + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.19.tgz", + "integrity": "sha512-ZiYsoiVRtDjxs5eDqtUAZrEsUWMEZ9+q5XpSOSHt+gTcZLBzntIOO21ZyAm6ONXyRO6EORsOy2vlnnOv87Cs0g==", "license": "MIT", "dependencies": { "axios": "^1.8.1" diff --git a/package.json b/package.json index 075819c..ed6debf 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@heroicons/react": "^2.2.0", "@microsoft/fetch-event-source": "^2.0.1", "@next/third-parties": "^15.3.0", - "@pharmatech/sdk": "^0.4.18", + "@pharmatech/sdk": "^0.4.19", "@radix-ui/react-slider": "^1.2.4", "@react-google-maps/api": "^2.20.6", "@types/google.maps": "^3.58.1", diff --git a/src/components/User/Order/UserOrderDetail.tsx b/src/components/User/Order/UserOrderDetail.tsx index 6cd67a2..f00b4ab 100644 --- a/src/components/User/Order/UserOrderDetail.tsx +++ b/src/components/User/Order/UserOrderDetail.tsx @@ -14,7 +14,6 @@ interface OrderDetailProps { orderNumber: string; products: OrderDetailResponse[]; subtotal: number; - discount: number; total: number; } @@ -22,13 +21,34 @@ export default function UserOrderDetail({ orderNumber, products, subtotal, - discount, total, }: OrderDetailProps) { + const now = new Date(); + + const totalDiscount = products.reduce((acc, item) => { + const presentation: OrderDetailProductPresentationResponse = + item.productPresentation; + const promo = presentation.promo; + const basePrice = presentation.price; + const quantity = item.quantity; + + const isPromoActive = + promo && + new Date(promo.startAt) <= now && + now < new Date(promo.expiredAt); + + if (isPromoActive) { + const discountedPrice = basePrice * (1 - promo.discount / 100); + const discountAmount = (basePrice - discountedPrice) * quantity; + return acc + discountAmount; + } + + return acc; + }, 0); + return (
- {/* Encabezado */} - + {/* Header */}

Pedido #{orderNumber.slice(0, 8)} @@ -43,18 +63,27 @@ export default function UserOrderDetail({

+
- {/* Lista con scroll en mobile */} + {/* Order Summary*/}
{products.map((item, idx) => { const presentation: OrderDetailProductPresentationResponse = item.productPresentation; const product = presentation.product; + const basePrice = presentation.price; + const quantity = item.quantity; + const promo = presentation.promo; - const originalPrice = presentation.promo - ? presentation.price * item.quantity - : null; + const isPromoActive = + promo && + new Date(promo.startAt) <= now && + now < new Date(promo.expiredAt); + + const discountedPrice = isPromoActive + ? basePrice * (1 - promo.discount / 100) + : basePrice; return (
- {/* Imagen */}
- {/* Nombre y cantidad */}

{product.name}

- Cantidad: {item.quantity} + Cantidad: {quantity}
- {/* Descripción */}

{product.description}

- {/* Precios */}
- {originalPrice && ( + {isPromoActive && ( - ${originalPrice.toFixed(2)} + ${(basePrice * quantity).toFixed(2)} )} - ${item.subtotal.toFixed(2)} + ${(discountedPrice * quantity).toFixed(2)}
- {/* Ir al producto */}
{product.description}

-
-
{item.quantity}
+
{quantity}
- {originalPrice && ( + {isPromoActive && ( - ${originalPrice.toFixed(2)} + ${(basePrice * quantity).toFixed(2)} )} - ${item.subtotal.toFixed(2)} + ${(discountedPrice * quantity).toFixed(2)}
@@ -179,7 +202,7 @@ export default function UserOrderDetail({ })}
- {/* Resumen del pedido */} + {/* TOTALS */}
@@ -194,7 +217,7 @@ export default function UserOrderDetail({
Descuentos
-
-${discount.toFixed(2)}
+
-${totalDiscount.toFixed(2)}
From 301ecc3a6a343a101f8a7c07c145c1f115aa0b51 Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Wed, 21 May 2025 00:26:36 -0400 Subject: [PATCH 32/50] validation taking the event as a string msg --- src/components/Navbar.tsx | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 300cbc8..a81f8e3 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import Link from 'next/link'; import Image from 'next/image'; import { ShoppingCartIcon, UserCircleIcon } from '@heroicons/react/24/outline'; @@ -39,6 +39,7 @@ export default function NavBar({ onCartClick }: NavBarProps) { const router = useRouter(); const [categories, setCategories] = useState([]); const [isNotificationsOpen, setIsNotificationsOpen] = useState(false); + const lastMsgRef = useRef(null); const { cartItems } = useCart(); const { token, user, isLoading } = useAuth(); @@ -137,20 +138,15 @@ export default function NavBar({ onCartClick }: NavBarProps) { if (res.ok) console.log('SSE abierta'); }, onmessage(ev) { - if (!ev.data) return; + const msg = ev.data?.trim(); - try { - const d = JSON.parse(ev.data); - if (d.type !== 'notification') return; + if (!msg || msg === lastMsgRef.current) { + return; + } - setNotifications((prev) => { - const exists = prev.some((n) => n.id === d.payload.id); - if (exists) return prev; + lastMsgRef.current = msg; - setNotificationCount((c) => c + 1); - return [d.payload, ...prev]; - }); - } catch {} + setNotificationCount((c) => c + 1); }, onclose() { if (!aborted) retryId = setTimeout(connect, 5000); From 6dab2f6ea42a48392ef5144a0d496e8e8ecec10d Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Wed, 21 May 2025 00:32:59 -0400 Subject: [PATCH 33/50] restore name in variant style badge --- src/components/Badge.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 2f45f26..5cb0952 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -99,7 +99,7 @@ const Badge: React.FC = ({ info: { color: Colors.semanticInfo }, }; - const styleByVariant = { + const variantStyle = { filled: colorClasses[color], outlined: outlinedClasses[color], text: textClasses[color], @@ -108,7 +108,7 @@ const Badge: React.FC = ({ return ( {children} From b7e49afc80ba6fdfb343ec35e46bf304f118f7e4 Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Fri, 23 May 2025 02:40:00 -0400 Subject: [PATCH 34/50] hook for notifications --- src/components/Navbar.tsx | 108 +++-------------- src/components/User/NotificationBell.tsx | 4 + src/lib/utils/helpers/useNotificationList.ts | 116 +++++++++++++++++++ 3 files changed, 136 insertions(+), 92 deletions(-) create mode 100644 src/lib/utils/helpers/useNotificationList.ts diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index a81f8e3..e9a864d 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,10 +1,9 @@ 'use client'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import Link from 'next/link'; import Image from 'next/image'; import { ShoppingCartIcon, UserCircleIcon } from '@heroicons/react/24/outline'; -import { fetchEventSource } from '@microsoft/fetch-event-source'; import Avatar from '@/components/Avatar'; import SearchBar from '@/components/SearchBar'; import { useRouter } from 'next/navigation'; @@ -13,14 +12,11 @@ import { Colors } from '../styles/styles'; import Button from '@/components/Button'; import { useCart } from '@/context/CartContext'; import { useAuth } from '@/context/AuthContext'; -import { api, API_URL } from '@/lib/sdkConfig'; -import { - CategoryResponse, - Pagination, - NotificationResponse, -} from '@pharmatech/sdk'; +import { api } from '@/lib/sdkConfig'; +import { CategoryResponse, Pagination } from '@pharmatech/sdk'; import CartOverlay from './Cart/CartOverlay'; import NotificationBell from '@/components/User/NotificationBell'; +import { useNotifications } from '@/lib/utils/helpers/useNotificationList'; interface UserProfile { id: string; @@ -38,54 +34,22 @@ type NavBarProps = { export default function NavBar({ onCartClick }: NavBarProps) { const router = useRouter(); const [categories, setCategories] = useState([]); - const [isNotificationsOpen, setIsNotificationsOpen] = useState(false); - const lastMsgRef = useRef(null); const { cartItems } = useCart(); const { token, user, isLoading } = useAuth(); + const { + notifications, + notificationCount, + isNotificationsOpen, + toggleNotifications, + panelRef, + } = useNotifications(token ?? undefined); const totalCount = cartItems.reduce((acc, item) => acc + item.quantity, 0); const [isLoggedIn, setIsLoggedIn] = useState(false); const [userData, setUserData] = useState(null); const [isCartOpen, setIsCartOpen] = useState(false); - const [notificationCount, setNotificationCount] = useState(0); - const [showLogin, setShowLogin] = useState(false); - const [notifications, setNotifications] = useState( - [], - ); - - const handleNotificationToggle = async () => { - const willOpen = !isNotificationsOpen; - - if (willOpen && token) { - try { - const res = await api.notification.getNotifications(token); - if (Array.isArray(res)) { - setNotifications(res); - - const unread = res.filter((n) => !n.isRead); - setNotificationCount(unread.length); - if (unread.length > 0) { - await Promise.all( - unread.map((notif) => - api.notification.markAsRead(notif.order.id, token), - ), - ); - setNotifications((prev) => - prev.map((n) => - unread.some((u) => u.id === n.id) ? { ...n, isRead: true } : n, - ), - ); - setNotificationCount(0); - } - } - } catch (err) { - console.error('Error fetching notifications:', err); - } - } - - setIsNotificationsOpen(willOpen); - }; + const [showLogin, setShowLogin] = useState(false); useEffect(() => { api.category @@ -123,48 +87,6 @@ export default function NavBar({ onCartClick }: NavBarProps) { return () => clearTimeout(timeout); }, []); - // Fetch notifications stream - useEffect(() => { - if (!token) return; - - let aborted = false; - let retryId: NodeJS.Timeout; - - const connect = () => { - fetchEventSource(`${API_URL}/notification/stream`, { - headers: { Authorization: `Bearer ${token}` }, - openWhenHidden: true, - async onopen(res) { - if (res.ok) console.log('SSE abierta'); - }, - onmessage(ev) { - const msg = ev.data?.trim(); - - if (!msg || msg === lastMsgRef.current) { - return; - } - - lastMsgRef.current = msg; - - setNotificationCount((c) => c + 1); - }, - onclose() { - if (!aborted) retryId = setTimeout(connect, 5000); - }, - onerror() { - if (!aborted) retryId = setTimeout(connect, 5000); - }, - }); - }; - - connect(); - - return () => { - aborted = true; - clearTimeout(retryId); - }; - }, [token]); - const handleSearch = (query: string, category: string) => { console.log('Buscando:', query, 'en', category); }; @@ -218,8 +140,9 @@ export default function NavBar({ onCartClick }: NavBarProps) { isMobile={false} notificationCount={notificationCount} isOpen={isNotificationsOpen} - onToggle={handleNotificationToggle} + onToggle={toggleNotifications} notifications={notifications} + panelRef={panelRef} /> {isLoggedIn && userData ? ( @@ -278,8 +201,9 @@ export default function NavBar({ onCartClick }: NavBarProps) { isMobile notificationCount={notificationCount} isOpen={isNotificationsOpen} - onToggle={handleNotificationToggle} + onToggle={toggleNotifications} notifications={notifications} + panelRef={panelRef} />
diff --git a/src/components/User/NotificationBell.tsx b/src/components/User/NotificationBell.tsx index 29c7571..c4aae16 100644 --- a/src/components/User/NotificationBell.tsx +++ b/src/components/User/NotificationBell.tsx @@ -3,6 +3,7 @@ import { BellIcon } from '@heroicons/react/24/outline'; import NotificationList from '@/components/User/NotificationList'; import type { NotificationResponse } from '@pharmatech/sdk'; +import { RefObject } from 'react'; interface Props { isMobile?: boolean; @@ -10,6 +11,7 @@ interface Props { isOpen: boolean; onToggle: () => void; notifications: NotificationResponse[]; + panelRef: RefObject; } export default function NotificationBell({ @@ -18,6 +20,7 @@ export default function NotificationBell({ isOpen, onToggle, notifications, + panelRef, }: Props) { return (
@@ -34,6 +37,7 @@ export default function NotificationBell({ {isOpen && (
( + [], + ); + const [notificationCount, setNotificationCount] = useState(0); + const [isNotificationsOpen, setIsNotificationsOpen] = useState(false); + const lastMsgRef = useRef(null); + const panelRef = useRef(null); + + const fetchNotifications = async (markAsRead = false) => { + if (!token) return; + try { + const res = await api.notification.getNotifications(token); + if (Array.isArray(res)) { + const unread = res.filter((n) => !n.isRead); + setNotifications(res); + setNotificationCount(unread.length); + if (markAsRead && unread.length > 0) { + await Promise.all( + unread.map((notif) => + api.notification.markAsRead(notif.order.id, token), + ), + ); + setNotifications((prev) => + prev.map((n) => + unread.some((u) => u.id === n.id) ? { ...n, isRead: true } : n, + ), + ); + setNotificationCount(0); + } + } + } catch (err) { + console.error('Error fetching notifications:', err); + } + }; + + const toggleNotifications = async () => { + const willOpen = !isNotificationsOpen; + setIsNotificationsOpen(willOpen); + if (willOpen) { + await fetchNotifications(true); + } + }; + + useEffect(() => { + if (!token) return; + fetchNotifications(); + }, [token]); + + useEffect(() => { + if (!token) return; + let aborted = false; + let retryId: NodeJS.Timeout; + const connect = () => { + fetchEventSource(`${API_URL}/notification/stream`, { + headers: { Authorization: `Bearer ${token}` }, + openWhenHidden: true, + onopen: async (res) => { + if (res.ok) console.log('SSE abierta'); + }, + onmessage(ev) { + const msg = ev.data?.trim(); + if (!msg || msg === lastMsgRef.current) return; + lastMsgRef.current = msg; + setNotificationCount((c) => c + 1); + }, + onclose() { + if (!aborted) retryId = setTimeout(connect, 5000); + }, + onerror() { + if (!aborted) retryId = setTimeout(connect, 5000); + }, + }); + }; + connect(); + return () => { + aborted = true; + clearTimeout(retryId); + }; + }, [token]); + + const handleClickOutside = useCallback( + (event: MouseEvent) => { + if ( + isNotificationsOpen && + panelRef.current && + !panelRef.current.contains(event.target as Node) + ) { + setIsNotificationsOpen(false); + } + }, + [isNotificationsOpen], + ); + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [handleClickOutside]); + + return { + notifications, + notificationCount, + isNotificationsOpen, + toggleNotifications, + panelRef, + }; +} From 9b2a13b223279b039bba112377cbb5b75e27b5d3 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 25 May 2025 11:30:29 -0400 Subject: [PATCH 35/50] Fix context --- package-lock.json | 62 ++++++------------ package.json | 1 - src/app/register/RegisterForm.tsx | 1 - src/context/AuthContext.tsx | 19 +++--- src/context/CartContext.tsx | 101 +++++------------------------- 5 files changed, 47 insertions(+), 137 deletions(-) diff --git a/package-lock.json b/package-lock.json index fc8fbde..c7f30e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@heroicons/react": "^2.2.0", "@microsoft/fetch-event-source": "^2.0.1", "@next/third-parties": "^15.3.0", - "@pharmatech/sdk": "^0.4.19", "@radix-ui/react-slider": "^1.2.4", "@react-google-maps/api": "^2.20.6", "@types/google.maps": "^3.58.1", @@ -1927,15 +1926,6 @@ "node": ">=12.4.0" } }, - "node_modules/@pharmatech/sdk": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.19.tgz", - "integrity": "sha512-ZiYsoiVRtDjxs5eDqtUAZrEsUWMEZ9+q5XpSOSHt+gTcZLBzntIOO21ZyAm6ONXyRO6EORsOy2vlnnOv87Cs0g==", - "license": "MIT", - "dependencies": { - "axios": "^1.8.1" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3548,6 +3538,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, "license": "MIT" }, "node_modules/available-typed-arrays": { @@ -3576,16 +3567,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", - "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3714,6 +3695,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3998,6 +3980,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -4248,6 +4231,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -4309,6 +4293,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4508,6 +4493,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4517,6 +4503,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4561,6 +4548,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4573,6 +4561,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5290,25 +5279,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -5345,6 +5315,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5437,6 +5408,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5461,6 +5433,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5611,6 +5584,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5689,6 +5663,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5701,6 +5676,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -6883,6 +6859,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6921,6 +6898,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6930,6 +6908,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -10293,11 +10272,6 @@ "react-is": "^16.13.1" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index ed6debf..c48ceef 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "@heroicons/react": "^2.2.0", "@microsoft/fetch-event-source": "^2.0.1", "@next/third-parties": "^15.3.0", - "@pharmatech/sdk": "^0.4.19", "@radix-ui/react-slider": "^1.2.4", "@react-google-maps/api": "^2.20.6", "@types/google.maps": "^3.58.1", diff --git a/src/app/register/RegisterForm.tsx b/src/app/register/RegisterForm.tsx index b1b360c..42978af 100644 --- a/src/app/register/RegisterForm.tsx +++ b/src/app/register/RegisterForm.tsx @@ -141,7 +141,6 @@ export default function RegisterForm() { }); login(loginResponse.accessToken, false); - window.location.reload(); router.push('/'); } catch (err) { console.error('Error creating account:', err); diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index e791c1c..2fd05e1 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -48,6 +48,13 @@ const decodeToken = (rawToken: string): JwtPayload | null => { } }; +const getToken = (): string | null => { + const storedToken = + localStorage.getItem('pharmatechToken') || + sessionStorage.getItem('pharmatechToken'); + return storedToken; +}; + export const AuthProvider = ({ children }: { children: ReactNode }) => { const [token, setToken] = useState(null); const [user, setUser] = useState(null); @@ -56,9 +63,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const router = useRouter(); useEffect(() => { - const storedToken = - localStorage.getItem('pharmatechToken') || - sessionStorage.getItem('pharmatechToken'); + const storedToken = getToken(); if (storedToken) { setToken(storedToken); @@ -70,7 +75,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { useEffect(() => { const syncLogout = () => { - const storedToken = localStorage.getItem('pharmatechToken'); + const storedToken = getToken(); setToken(storedToken); if (storedToken) { @@ -86,9 +91,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { }, []); const login = (newToken: string, remember: boolean) => { - localStorage.setItem('pharmatechToken', newToken); - if (!remember) { - sessionStorage.setItem('pharmatechToken', newToken); + sessionStorage.setItem('pharmatechToken', newToken); + if (remember) { + localStorage.setItem('pharmatechToken', newToken); } setToken(newToken); diff --git a/src/context/CartContext.tsx b/src/context/CartContext.tsx index a493b8f..c108892 100644 --- a/src/context/CartContext.tsx +++ b/src/context/CartContext.tsx @@ -6,8 +6,8 @@ import React, { useEffect, ReactNode, useRef, + useCallback, } from 'react'; -import { useAuth } from '@/context/AuthContext'; import { toast } from 'react-toastify'; export interface CartItem { @@ -34,19 +34,6 @@ const CartContext = createContext(undefined); export const CartProvider = ({ children }: { children: ReactNode }) => { const [cartItems, setCartItems] = useState([]); const alertShownRef = useRef(false); - const { token, user } = useAuth(); - console.log('Token in Cart Provider:', token); - console.log('User in Cart Provider:', user); - - const showStockAlert = () => { - if (!alertShownRef.current) { - alertShownRef.current = true; - toast.error('No hay suficiente stock para este producto.'); - setTimeout(() => { - alertShownRef.current = false; - }, 1000); - } - }; useEffect(() => { const storedCart = localStorage.getItem('cartItems'); @@ -63,68 +50,17 @@ export const CartProvider = ({ children }: { children: ReactNode }) => { localStorage.setItem('cartItems', JSON.stringify(cartItems)); }, [cartItems]); - // Al detectar cambios en el token: - // - Si existe y además tenemos el usuario decodificado, se fusiona el carrito local con el del usuario (si no se ha fusionado ya). - // - Si no hay token, se mantiene el carrito del usuario anónimo. - useEffect(() => { - if (token && user) { - const userId = user.sub; - // Verificar si ya se fusionó el carrito para este usuario usando un flag en localStorage - const mergedUser = localStorage.getItem('mergedUser'); - if (mergedUser === userId) { - // Ya se realizó la fusión para este usuario - return; - } - - // Aquí se debería obtener el carrito del usuario desde la API. - // Descomenta y ajusta el siguiente bloque cuando el endpoint esté listo: - /* - api.cart.getUserCart(token) - .then((serverCart: CartItem[]) => { - const localCart: CartItem[] = JSON.parse(localStorage.getItem('cartItems') || '[]'); - const mergedCart = mergeCarts(serverCart, localCart); - setCartItems(mergedCart); - localStorage.setItem('cartItems', JSON.stringify(mergedCart)); - // Guardar el userId para evitar fusiones repetidas - localStorage.setItem('mergedUser', userId); - }) - .catch((error) => { - console.error('Error al obtener el carrito del usuario:', error); - }); - */ - - // Simulación: Se fusiona el carrito local con un carrito de servidor vacío - const localCart: CartItem[] = JSON.parse( - localStorage.getItem('cartItems') || '[]', - ); - const mergedCart = mergeCarts([], localCart); - setCartItems(mergedCart); - localStorage.setItem('cartItems', JSON.stringify(mergedCart)); - localStorage.setItem('mergedUser', userId); + const showStockAlert = useCallback(() => { + if (!alertShownRef.current) { + alertShownRef.current = true; + toast.error('No hay suficiente stock para este producto.'); + setTimeout(() => { + alertShownRef.current = false; + }, 1000); } - // Si no hay token, se mantiene el carrito guardado sin limpiarlo. - }, [token, user]); - - // Función para fusionar dos carritos sumando las cantidades en caso de que se repita el mismo ítem - const mergeCarts = ( - serverCart: CartItem[], - localCart: CartItem[], - ): CartItem[] => { - const merged = [...serverCart]; - localCart.forEach((localItem) => { - const index = merged.findIndex((item) => item.id === localItem.id); - if (index !== -1) { - // Si el ítem ya existe, se suman las cantidades - merged[index].quantity += localItem.quantity; - } else { - merged.push(localItem); - } - }); - return merged; - }; + }, []); - // Función para agregar un ítem al carrito - const addItem = (item: CartItem) => { + const addItem = useCallback((item: CartItem) => { setCartItems((prev) => { const exists = prev.find((p) => p.id === item.id); if (exists) { @@ -147,10 +83,9 @@ export const CartProvider = ({ children }: { children: ReactNode }) => { } return [...prev, item]; }); - }; + }, []); - // Función para actualizar la cantidad de un ítem del carrito - const updateItemQuantity = (id: string, quantity: number) => { + const updateItemQuantity = useCallback((id: string, quantity: number) => { setCartItems((prev) => prev.map((item) => { if (item.id === id) { @@ -169,19 +104,17 @@ export const CartProvider = ({ children }: { children: ReactNode }) => { return item; }), ); - }; + }, []); - // Función para eliminar un ítem del carrito - const removeItem = (id: string) => { + const removeItem = useCallback((id: string) => { setCartItems((prev) => prev.filter((item) => item.id !== id)); - }; + }, []); - // Función para vaciar el carrito (por ejemplo, al hacer logout) - const clearCart = () => { + const clearCart = useCallback(() => { setCartItems([]); localStorage.removeItem('cartItems'); localStorage.removeItem('mergedUser'); - }; + }, []); return ( Date: Sun, 25 May 2025 11:56:22 -0400 Subject: [PATCH 36/50] Standarize icons and bubbles in navbar --- src/components/Avatar.tsx | 4 ++- src/components/Navbar.tsx | 71 ++++++++++++++++++++----------------- src/context/CartContext.tsx | 16 ++++++++- 3 files changed, 56 insertions(+), 35 deletions(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index f1ca606..aa18fb1 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -5,6 +5,7 @@ import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { useAuth } from '@/context/AuthContext'; import { Colors } from '@/styles/styles'; +import { useCart } from '@/context/CartContext'; export type AvatarProps = { name: string; @@ -33,6 +34,7 @@ export default function Avatar({ const dropdownRef = useRef(null); const router = useRouter(); const { logout, token } = useAuth(); + const { clearCart } = useCart(); const initials = name .split(' ') @@ -50,8 +52,8 @@ export default function Avatar({ const handleLogoutClick = () => { logout(); + clearCart(); setDropdownOpen(false); - router.push('/'); }; useEffect(() => { diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index e9a864d..8169edd 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -34,7 +34,7 @@ type NavBarProps = { export default function NavBar({ onCartClick }: NavBarProps) { const router = useRouter(); const [categories, setCategories] = useState([]); - const { cartItems } = useCart(); + const { itemsCount } = useCart(); const { token, user, isLoading } = useAuth(); const { notifications, @@ -43,8 +43,6 @@ export default function NavBar({ onCartClick }: NavBarProps) { toggleNotifications, panelRef, } = useNotifications(token ?? undefined); - - const totalCount = cartItems.reduce((acc, item) => acc + item.quantity, 0); const [isLoggedIn, setIsLoggedIn] = useState(false); const [userData, setUserData] = useState(null); const [isCartOpen, setIsCartOpen] = useState(false); @@ -131,28 +129,31 @@ export default function NavBar({ onCartClick }: NavBarProps) { onClick={() => setIsCartOpen(true)} > - - {totalCount} - + {itemsCount > 0 && ( + + {itemsCount} + + )}
- - {isLoggedIn && userData ? ( - router.push('/user')} - /> + <> + + router.push('/user')} + /> + ) : showLogin ? (
diff --git a/src/context/CartContext.tsx b/src/context/CartContext.tsx index c108892..356f22c 100644 --- a/src/context/CartContext.tsx +++ b/src/context/CartContext.tsx @@ -23,6 +23,7 @@ export interface CartItem { interface CartContextProps { cartItems: CartItem[]; + itemsCount: number; addItem: (item: CartItem) => void; updateItemQuantity: (id: string, quantity: number) => void; removeItem: (id: string) => void; @@ -33,6 +34,7 @@ const CartContext = createContext(undefined); export const CartProvider = ({ children }: { children: ReactNode }) => { const [cartItems, setCartItems] = useState([]); + const [itemsCount, setItemsCount] = useState(0); const alertShownRef = useRef(false); useEffect(() => { @@ -60,6 +62,11 @@ export const CartProvider = ({ children }: { children: ReactNode }) => { } }, []); + useEffect(() => { + const totalItems = cartItems.reduce((acc, item) => acc + item.quantity, 0); + setItemsCount(totalItems); + }, [cartItems]); + const addItem = useCallback((item: CartItem) => { setCartItems((prev) => { const exists = prev.find((p) => p.id === item.id); @@ -118,7 +125,14 @@ export const CartProvider = ({ children }: { children: ReactNode }) => { return ( {children} From 7126ce8e1863577f56f42358aa4c46c262320030 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 25 May 2025 12:12:15 -0400 Subject: [PATCH 37/50] Set navbar on top of the page --- src/app/(shop)/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(shop)/layout.tsx b/src/app/(shop)/layout.tsx index bb0ec5b..353a291 100644 --- a/src/app/(shop)/layout.tsx +++ b/src/app/(shop)/layout.tsx @@ -18,7 +18,7 @@ export default function ShopLayout({ children }: ShopLayoutProps) {
{/* Nav */} -
+
{children} From 449b0d91f465519b74b94b5b008c90389453d415 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 25 May 2025 12:18:46 -0400 Subject: [PATCH 38/50] Fix margin from navbar and recommenden products state --- src/app/(shop)/page.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/(shop)/page.tsx b/src/app/(shop)/page.tsx index 936ac22..0914498 100644 --- a/src/app/(shop)/page.tsx +++ b/src/app/(shop)/page.tsx @@ -108,7 +108,10 @@ export default function Home() { useEffect(() => { const fetchRecommended = async () => { - if (!token || !user?.sub) return; + if (!token || !user?.sub) { + setRecommendedProducts([]); + return; + } try { const data = await api.product.getRecommendations(token); setRecommendedProducts(data.results); @@ -121,7 +124,7 @@ export default function Home() { return (
-

Pharmatech

+

Pharmatech

{/* Carrusel principal */}
From 4b5b69700911eb6887d00c2583d0eba09cb133b8 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 25 May 2025 18:44:15 -0400 Subject: [PATCH 39/50] Improve search --- src/app/(shop)/search/page.tsx | 262 ++++++++------------------- src/components/Navbar.tsx | 22 +-- src/components/SearchBar.tsx | 37 ++-- src/components/SidebarFilter.tsx | 153 ++++++---------- src/components/SuggestionProduct.tsx | 7 +- 5 files changed, 159 insertions(+), 322 deletions(-) diff --git a/src/app/(shop)/search/page.tsx b/src/app/(shop)/search/page.tsx index 7854e85..bfec5c5 100644 --- a/src/app/(shop)/search/page.tsx +++ b/src/app/(shop)/search/page.tsx @@ -1,218 +1,104 @@ 'use client'; -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import SidebarFilter, { Filters } from '@/components/SidebarFilter'; import ProductCard from '@/components/Product/ProductCard'; import { api } from '@/lib/sdkConfig'; import Breadcrumb from '@/components/Breadcrumb'; import Loading from '@/app/loading'; -import { ProductPaginationRequest, ProductPresentation } from '@pharmatech/sdk'; - -interface CategoryOption { - id: string; - name: string; -} +import { + isAPIError, + ProductPaginationRequest, + ProductPresentation, +} from '@pharmatech/sdk'; export default function SearchPage() { const router = useRouter(); const params = useSearchParams(); - const query = params?.get('query') || ''; - const categoryId = params?.get('categoryId') || ''; - const [customSearch, setCustomSearch] = useState(false); - const [lastSearchQuery, setLastSearchQuery] = useState(''); - const [allProducts, setAllProducts] = useState([]); + const query = params?.get('query') || ''; + const categories = params?.get('categoryId')?.split(',') || ['']; + const brands = params?.get('brand')?.split(',') || []; + const presentations = params?.get('presentation')?.split(',') || []; + const activeIngredients = params?.get('activeIngredient')?.split(',') || []; + const min = parseFloat(params?.get('priceMin') || String(0)); + const max = parseFloat(params?.get('priceMax') || String(10000)); const [displayProducts, setDisplayProducts] = useState( [], ); - const priceRange = useMemo<[number, number]>(() => [0, 1000], []); - const [currentPriceRange, setCurrentPriceRange] = - useState<[number, number]>(priceRange); const [loading, setLoading] = useState(false); const [showMobileFilters, setShowMobileFilters] = useState(false); - - const [categoriesList, setCategoriesList] = useState([]); const [currentFilters, setCurrentFilters] = useState({ - category: [], - brand: [], - presentation: [], - activeIngredient: [], + categories, + brands, + presentations, + activeIngredients, + query, + priceMin: min, + priceMax: max, }); - const [initialCategoryName, setInitialCategoryName] = useState(''); - - useEffect(() => { - api.category - .findAll({ page: 1, limit: 10 }) - .then((resp) => - setCategoriesList( - resp.results.map((c) => ({ id: c.id, name: c.name })), - ), - ) - .catch((err) => console.error('Error cargando categorías:', err)); - }, [params, priceRange, query]); - - useEffect(() => { - if (categoryId && categoriesList.length && !initialCategoryName) { - const found = categoriesList.find((c) => c.id === categoryId); - if (found) setInitialCategoryName(found.name); - } - }, [categoryId, categoriesList, initialCategoryName]); - - const selectedCategoryNames = - currentFilters.category.length > 0 - ? (currentFilters.category - .map((id) => categoriesList.find((c) => c.id === id)?.name) - .filter(Boolean) as string[]) - : initialCategoryName - ? [initialCategoryName] - : []; - - const breadcrumbItems = [ - { label: 'Inicio', href: '/' }, - ...(initialCategoryName ? [{ label: initialCategoryName }] : []), - ...(customSearch - ? [ - { - label: 'Búsqueda personalizada', - href: `/search?${lastSearchQuery}`, - }, - ] - : query - ? [{ label: query, href: `/search?query=${query}` }] - : []), - ]; - - const handleApplyFilters = async ( - filters: Filters, - price: [number, number], - ) => { - setCurrentFilters(filters); - setCurrentPriceRange(price); - setCustomSearch(true); - setLoading(true); - - const newParams = new URLSearchParams(); - if (filters.category.length) - newParams.set('categoryId', filters.category.join(',')); - if (filters.brand.length) newParams.set('brand', filters.brand.join(',')); - if (filters.presentation.length) - newParams.set('presentation', filters.presentation.join(',')); - if (filters.activeIngredient.length) - newParams.set('activeIngredient', filters.activeIngredient.join(',')); - newParams.set('priceMin', String(price[0])); - newParams.set('priceMax', String(price[1])); - - const paramString = newParams.toString(); - setLastSearchQuery(paramString); - await router.replace(`/search?${paramString}`); - - const req: ProductPaginationRequest = { - page: 1, - limit: 50, - ...(query.trim() && { q: query.trim() }), - ...(filters.brand.length > 0 && { manufacturerId: filters.brand }), - ...(filters.category.length > 0 && { categoryId: filters.category }), - ...(filters.activeIngredient.length > 0 && { - genericProductId: filters.activeIngredient, - }), - ...(filters.presentation.length > 0 && { - presentationId: filters.presentation, - }), - priceRange: { min: price[0], max: price[1] }, - }; - - try { - const resp = await api.product.getProducts(req); - setDisplayProducts(resp.results); - } catch (err) { - console.error('Error aplicando filtros:', err); - } finally { - setLoading(false); - setShowMobileFilters(false); - } - }; - useEffect(() => { - const rawCat = params?.get('categoryId') || ''; - const cat = rawCat ? rawCat.split(',') : []; - const brand = params?.get('brand')?.split(',') || []; - const pres = params?.get('presentation')?.split(',') || []; - const act = params?.get('activeIngredient')?.split(',') || []; - const min = parseFloat(params?.get('priceMin') || String(priceRange[0])); - const max = parseFloat(params?.get('priceMax') || String(priceRange[1])); - const hasFilters = - cat.length > 0 || - brand.length > 0 || - pres.length > 0 || - act.length > 0 || - params?.get('priceMin') != null; - - if (hasFilters) { - const filters: Filters = { - category: cat, - brand, - presentation: pres, - activeIngredient: act, - }; - setLastSearchQuery(params ? params.toString() : ''); - + const fetchProducts = async () => { const req: ProductPaginationRequest = { page: 1, limit: 50, - ...(query.trim() && { q: query.trim() }), - ...(cat.length > 0 && { categoryId: cat }), - ...(brand.length > 0 && { manufacturerId: brand }), - ...(act.length > 0 && { genericProductId: act }), - ...(pres.length > 0 && { presentationId: pres }), - priceRange: { min, max }, + ...(query.trim() && { q: currentFilters.query?.trim() }), + ...(categories.length > 0 && { + categoryId: currentFilters.categories.filter((c) => c != ''), + }), + ...(brands.length > 0 && { manufacturerId: currentFilters.brands }), + ...(activeIngredients.length > 0 && { + genericProductId: currentFilters.activeIngredients, + }), + ...(presentations.length > 0 && { + presentationId: currentFilters.presentations, + }), + ...(currentFilters.priceMin && + currentFilters.priceMax && { + priceRange: { + min: currentFilters.priceMin, + max: currentFilters.priceMax, + }, + }), }; - api.product - .getProducts(req) - .then((data) => { - setDisplayProducts(data.results); - setCurrentFilters(filters); - setCurrentPriceRange([min, max]); - setCustomSearch(true); - }) - .catch((err) => - console.error('Error cargando con filtros al montar:', err), - ) - .finally(() => setLoading(false)); - } - }, [params, priceRange, query]); - - useEffect(() => { - if (!customSearch) { setLoading(true); - const req: ProductPaginationRequest = { page: 1, limit: 50 }; - if (query.trim()) req.q = query.trim(); - if (categoryId) req.categoryId = [categoryId]; + try { + const products = await api.product.getProducts(req); + setDisplayProducts(products.results); + } catch (error) { + setDisplayProducts([]); + if (isAPIError(error)) { + console.error('Error fetching products:', error.message); + } else { + console.error('Unexpected error fetching products:', error); + } + } finally { + setLoading(false); + } + }; - api.product - .getProducts(req) - .then((data) => { - setAllProducts(data.results); - setDisplayProducts(data.results); - }) - .catch((err) => console.error('Error cargando productos:', err)) - .finally(() => setLoading(false)); - } - }, [query, categoryId, customSearch]); + fetchProducts(); + }, [params, currentFilters]); + + const breadcrumbItems = [ + { label: 'Inicio', href: '/' }, + ...(query ? [{ label: 'Búsqueda', href: `/search?query=${query}` }] : []), + ]; const handleClearFilters = () => { setCurrentFilters({ - category: [], - brand: [], - presentation: [], - activeIngredient: [], + categories: [], + brands: [], + presentations: [], + activeIngredients: [], + query: '', + priceMin: 0, + priceMax: 10000, }); - setDisplayProducts(allProducts); - setCurrentPriceRange(priceRange); - setCustomSearch(false); setShowMobileFilters(false); - router.replace('/search'); + router.push('/search'); }; return ( @@ -231,10 +117,8 @@ export default function SearchPage() {
@@ -244,10 +128,8 @@ export default function SearchPage() { @@ -267,9 +149,7 @@ export default function SearchPage() {

Resultados para:{' '} - {selectedCategoryNames.length > 0 - ? selectedCategoryNames.join(', ') - : query || 'Todos los productos'} + {query ? query : 'Todos los productos'}

diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 8169edd..c24c4a3 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -13,7 +13,6 @@ import Button from '@/components/Button'; import { useCart } from '@/context/CartContext'; import { useAuth } from '@/context/AuthContext'; import { api } from '@/lib/sdkConfig'; -import { CategoryResponse, Pagination } from '@pharmatech/sdk'; import CartOverlay from './Cart/CartOverlay'; import NotificationBell from '@/components/User/NotificationBell'; import { useNotifications } from '@/lib/utils/helpers/useNotificationList'; @@ -33,7 +32,7 @@ type NavBarProps = { export default function NavBar({ onCartClick }: NavBarProps) { const router = useRouter(); - const [categories, setCategories] = useState([]); + const { itemsCount } = useCart(); const { token, user, isLoading } = useAuth(); const { @@ -49,17 +48,6 @@ export default function NavBar({ onCartClick }: NavBarProps) { const [showLogin, setShowLogin] = useState(false); - useEffect(() => { - api.category - .findAll({ page: 1, limit: 20 }) - .then((resp: Pagination) => { - if (resp?.results) setCategories(resp.results); - }) - .catch((err) => { - console.error('Error al cargar categorías:', err); - }); - }, []); - useEffect(() => { if (token && user?.sub) { setIsLoggedIn(true); @@ -85,10 +73,6 @@ export default function NavBar({ onCartClick }: NavBarProps) { return () => clearTimeout(timeout); }, []); - const handleSearch = (query: string, category: string) => { - console.log('Buscando:', query, 'en', category); - }; - const handleLoginClick = () => { router.push('/login'); }; @@ -112,8 +96,6 @@ export default function NavBar({ onCartClick }: NavBarProps) { /> void; width?: string; height?: string; borderRadius?: string; @@ -24,9 +23,13 @@ type SearchBarProps = { disableDropdown?: boolean; }; +const defaultCategory: CategoryResponse = { + name: 'Categorías', + id: '', + description: '', +}; + export default function SearchBar({ - categories, - onSearch, width = '100%', height = '2.5rem', borderRadius = '0.375rem', @@ -38,20 +41,26 @@ export default function SearchBar({ disableDropdown = false, }: SearchBarProps) { const router = useRouter(); - const [selectedCategory, setSelectedCategory] = useState({ - name: 'Categorías', - id: '1', - description: '', - }); + const [categories, setCategories] = useState([]); + const [selectedCategory, setSelectedCategory] = + useState(defaultCategory); const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); + useEffect(() => { + api.category + .findAll({ page: 1, limit: 20 }) + .then((resp: Pagination) => { + if (resp?.results) setCategories([defaultCategory, ...resp.results]); + }) + .catch((err) => { + console.error('Error al cargar categorías:', err); + }); + }, []); + const handleSearch = () => { const term = searchTerm.trim(); if (!term) return; - - onSearch?.(term, selectedCategory.name); - const q = encodeURIComponent(term); const categoryId = encodeURIComponent(selectedCategory.id.trim()); router.push(`/search?query=${q}&categoryId=${categoryId}`); diff --git a/src/components/SidebarFilter.tsx b/src/components/SidebarFilter.tsx index d4a0e01..4575535 100644 --- a/src/components/SidebarFilter.tsx +++ b/src/components/SidebarFilter.tsx @@ -1,7 +1,7 @@ 'use client'; -import React, { useState, useEffect, useRef } from 'react'; -import { useRouter, usePathname, useSearchParams } from 'next/navigation'; +import React, { useState, useEffect, useMemo } from 'react'; +import { useRouter } from 'next/navigation'; import { api } from '@/lib/sdkConfig'; import CheckButton from '@/components/CheckButton'; import { @@ -12,17 +12,18 @@ import { } from '@radix-ui/react-slider'; export interface Filters { - category: string[]; - brand: string[]; - presentation: string[]; - activeIngredient: string[]; + categories: string[]; + brands: string[]; + presentations: string[]; + activeIngredients: string[]; + query?: string; + priceMin?: number; + priceMax?: number; } interface SidebarFilterProps { initialFilters: Filters; - initialPriceRange?: [number, number]; - initialCurrentPriceRange?: [number, number]; - onApplyFilters: (filters: Filters, priceRange: [number, number]) => void; + setFilters: (filters: Filters) => void; onClearFilters: () => void; } @@ -33,53 +34,17 @@ interface Option { export default function SidebarFilter({ initialFilters, - initialPriceRange = [0, 0], - initialCurrentPriceRange = [0, 0], - onApplyFilters, onClearFilters, + setFilters, }: SidebarFilterProps) { + const priceRange = useMemo<[number, number]>(() => [0, 10000], []); const router = useRouter(); - const pathname = usePathname() || '/'; - const searchParams = useSearchParams(); const [localFilters, setLocalFilters] = useState(initialFilters); - const [localPrice, setLocalPrice] = useState<[number, number]>( - initialCurrentPriceRange, - ); const [categoriesList, setCategoriesList] = useState([]); const [brandsList, setBrandsList] = useState([]); const [presentationsList, setPresentationsList] = useState([]); - const initialSyncRef = useRef(true); - - useEffect(() => { - if (initialSyncRef.current) { - const params = searchParams; - const rawCategory = - params?.get('category') || params?.get('categoryId') || ''; - const category = rawCategory ? rawCategory.split(',') : []; - const brand = params?.get('brand')?.split(',') || []; - const presentation = params?.get('presentation')?.split(',') || []; - const activeIngredient = - params?.get('activeIngredient')?.split(',') || []; - const priceMin = parseFloat( - params?.get('priceMin') || String(initialPriceRange[0]), - ); - const priceMax = parseFloat( - params?.get('priceMax') || String(initialPriceRange[1]), - ); - - setLocalFilters({ category, brand, presentation, activeIngredient }); - setLocalPrice([priceMin, priceMax]); - // Trigger parent to reload results on refresh - onApplyFilters({ category, brand, presentation, activeIngredient }, [ - priceMin, - priceMax, - ]); - initialSyncRef.current = false; - } - }, [searchParams, initialPriceRange, onApplyFilters]); - const fetchOptions = async ( serviceFn: (opts: { page: number; limit: number }) => Promise<{ results: Array<{ @@ -130,46 +95,39 @@ export default function SidebarFilter({ }, []); const handleApply = () => { - const params = new URLSearchParams( - Array.from(searchParams?.entries() || []), - ); - - // Preserve initial category if none selected locally - const rawCat = params.get('category') || params.get('categoryId') || ''; - const initialCats = rawCat ? rawCat.split(',') : []; - const selectedCategories = - localFilters.category.length > 0 ? localFilters.category : initialCats; + const params = new URLSearchParams(); - if (localFilters.category.length) - params.set('categoryId', localFilters.category.join(',')); + if (localFilters.categories.length > 0) + // remove empty string from categories if length is greater than 1 + params.set( + 'categoryId', + localFilters.categories.filter((c) => c !== '').join(','), + ); else params.delete('categoryId'); - if (localFilters.brand.length > 0) - params.set('brand', localFilters.brand.join(',')); + if (localFilters.brands.length > 0) + params.set('brand', localFilters.brands.join(',')); else params.delete('brand'); - if (localFilters.presentation.length > 0) - params.set('presentation', localFilters.presentation.join(',')); + if (localFilters.presentations.length > 0) + params.set('presentation', localFilters.presentations.join(',')); else params.delete('presentation'); - if (localFilters.activeIngredient.length > 0) - params.set('activeIngredient', localFilters.activeIngredient.join(',')); + if (localFilters.activeIngredients.length > 0) + params.set('activeIngredient', localFilters.activeIngredients.join(',')); else params.delete('activeIngredient'); - params.set('priceMin', String(localPrice[0])); - params.set('priceMax', String(localPrice[1])); - - router.push(`${pathname}?${params.toString()}`); - onApplyFilters( - { - ...localFilters, - category: selectedCategories, - }, - localPrice, - ); + params.set('priceMin', String(localFilters.priceMin)); + params.set('priceMax', String(localFilters.priceMax)); + params.set('query', localFilters.query || ''); + setFilters(localFilters); + router.push(`/search?${params.toString()}`); }; - const toggleSelection = (key: keyof Filters, id: string) => { + const toggleSelection = ( + key: 'categories' | 'brands' | 'presentations', + id: string, + ) => { setLocalFilters((prev) => { const arr = prev[key]; const updated = arr.includes(id) @@ -179,16 +137,25 @@ export default function SidebarFilter({ }); }; + const setLocalPrice = (value: [number, number]) => { + setLocalFilters((prev) => ({ + ...prev, + priceMin: value[0], + priceMax: value[1], + })); + }; + const handleClear = () => { setLocalFilters({ - category: [], - brand: [], - presentation: [], - activeIngredient: [], + categories: [], + brands: [], + presentations: [], + activeIngredients: [], + query: '', + priceMin: priceRange[0], + priceMax: priceRange[1], }); - setLocalPrice(initialPriceRange); onClearFilters(); - router.push(pathname); }; const scrollClass = @@ -213,8 +180,8 @@ export default function SidebarFilter({
toggleSelection('category', opt.id)} + checked={localFilters.categories.includes(opt.id)} + onChange={() => toggleSelection('categories', opt.id)} />
))} @@ -228,8 +195,8 @@ export default function SidebarFilter({
toggleSelection('brand', opt.id)} + checked={localFilters.brands.includes(opt.id)} + onChange={() => toggleSelection('brands', opt.id)} />
))} @@ -243,8 +210,8 @@ export default function SidebarFilter({
toggleSelection('presentation', opt.id)} + checked={localFilters.presentations.includes(opt.id)} + onChange={() => toggleSelection('presentations', opt.id)} />
))} @@ -255,9 +222,9 @@ export default function SidebarFilter({

Precio

setLocalPrice([value[0], value[1]])} > @@ -267,8 +234,8 @@ export default function SidebarFilter({
- Bs {localPrice[0]} - Bs {localPrice[1]} + Bs {priceRange[0] / 100} + Bs {priceRange[1] / 100}
diff --git a/src/components/SuggestionProduct.tsx b/src/components/SuggestionProduct.tsx index 742a300..43922b0 100644 --- a/src/components/SuggestionProduct.tsx +++ b/src/components/SuggestionProduct.tsx @@ -42,8 +42,7 @@ export default function SearchSuggestions({ page: 1, limit: 10, ...(query.trim() && { q: query.trim() }), - ...(category.id && - category.id !== '1' && { categoryId: [category.id] }), + ...(category.id && { categoryId: [category.id] }), }; const data = await api.product.getProducts(params); setProducts(data.results); @@ -83,7 +82,9 @@ export default function SearchSuggestions({

Categoría

-

{category.name}

+

+ {category.name == 'Categorías' ? 'Todas' : category.name} +

From 44b4a7b3c4b9c46a134e439a5034accba1b4bf79 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 25 May 2025 19:08:31 -0400 Subject: [PATCH 40/50] Fix loader in product presentation detail --- .../presentation/[presentationId]/page.tsx | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx index 8814aa9..ee91f5f 100644 --- a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx +++ b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx @@ -34,15 +34,16 @@ export default function ProductDetailPage() { const presentationId = String(params?.presentationId || ''); const [presentation, setPresentation] = - useState(null); + useState(); const [genericProduct, setGenericProduct] = - useState(null); + useState(); const [slides, setSlides] = useState([]); const [products, setProducts] = useState([]); const [presentationList, setPresentationList] = useState< ProductPresentationResponse[] >([]); - const [loading, setLoading] = useState(true); + const [presentationIsLoading, setPresentationIsLoading] = useState(true); + const [productIsLoading, setProductIsLoading] = useState(true); // 1) Load presentation detail useEffect(() => { @@ -50,8 +51,11 @@ export default function ProductDetailPage() { api.productPresentation .getByPresentationId(productId, presentationId) .then((data) => setPresentation(data)) - .catch((err) => console.error(err)) - .finally(() => setLoading(false)); + .catch((err) => { + console.error(err); + setProductIsLoading(false); + }) + .finally(() => setPresentationIsLoading(false)); }, [productId, presentationId]); // 2) Load generic product info & variants @@ -87,7 +91,10 @@ export default function ProductDetailPage() { { id: 1, imageUrl: '/images/product-detail.jpg' }, { id: 2, imageUrl: '/images/product-detail-2.jpg' }, ]), - ); + ) + .finally(() => { + setProductIsLoading(false); + }); }, [genericProduct]); // 4) Fetch related products @@ -105,8 +112,14 @@ export default function ProductDetailPage() { .then((res) => setProducts(res.results)) .catch((err) => console.error(err)); }, [genericProduct]); - if (loading) return ; - if (!presentation || !genericProduct) return ; + + if (presentationIsLoading || productIsLoading) { + return ; + } + + if (!presentation || !genericProduct) { + return ; + } // Breadcrumb con acción de "volver" si es búsqueda personalizada const breadcrumbItems = [ @@ -153,6 +166,9 @@ export default function ProductDetailPage() {

{presentation.presentation.description}

+

+ Existencia: {presentation.stock || 0} +

${presentation.price.toFixed(2)} From 0858a8913ff4c47fc439bb3ad56625af21a79fe6 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 25 May 2025 19:51:49 -0400 Subject: [PATCH 41/50] Add missing component for order states --- src/app/(shop)/order/[id]/page.tsx | 14 ++++++++--- src/app/(shop)/user/address/page.tsx | 2 +- src/app/(shop)/user/layout.tsx | 2 +- src/components/Avatar.tsx | 9 ++++++- src/components/Order/PaymentProcess.tsx | 24 +++---------------- src/components/User/Order/UserOrderDetail.tsx | 9 ++++--- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/app/(shop)/order/[id]/page.tsx b/src/app/(shop)/order/[id]/page.tsx index 57bfad7..09632f6 100644 --- a/src/app/(shop)/order/[id]/page.tsx +++ b/src/app/(shop)/order/[id]/page.tsx @@ -139,19 +139,27 @@ export default function OrderInProgress() { case OrderStatus.REQUESTED: return ; case OrderStatus.APPROVED: - return ; + if ( + [PaymentMethod.BANK_TRANSFER, PaymentMethod.MOBILE_PAYMENT].includes( + order.paymentMethod, + ) + ) { + return ; + } else { + return ; + } case OrderStatus.IN_PROGRESS: return order.type === OrderType.PICKUP ? ( ) : ( ); + case OrderStatus.READY_FOR_PICKUP: + return ; case OrderStatus.CANCELED: return ; case OrderStatus.COMPLETED: return ; - default: - return

Error
; } }; diff --git a/src/app/(shop)/user/address/page.tsx b/src/app/(shop)/user/address/page.tsx index a557c97..1e267c0 100644 --- a/src/app/(shop)/user/address/page.tsx +++ b/src/app/(shop)/user/address/page.tsx @@ -61,7 +61,7 @@ export default function AddressPage() { } }; - if (!user || loading) return; //; + if (!user || loading) return; return ( <> diff --git a/src/app/(shop)/user/layout.tsx b/src/app/(shop)/user/layout.tsx index 61c2a8d..299251c 100644 --- a/src/app/(shop)/user/layout.tsx +++ b/src/app/(shop)/user/layout.tsx @@ -68,7 +68,7 @@ export default function UserProfileLayout({ })(); }, [user?.sub, token]); - if (!user?.sub || !userData) return; //; + if (!user?.sub || !userData) return; const sidebarUser: SidebarUser = { name: `${userData.firstName} ${userData.lastName}`, diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index aa18fb1..3509614 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -56,6 +56,13 @@ export default function Avatar({ setDropdownOpen(false); }; + const handleSafeProfileClick = () => { + if (onProfileClick) { + setDropdownOpen(false); + onProfileClick(); + } + }; + useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( @@ -116,7 +123,7 @@ export default function Avatar({ {onProfileClick && (
  • Ir a mi perfil
  • diff --git a/src/components/Order/PaymentProcess.tsx b/src/components/Order/PaymentProcess.tsx index 4b186ae..c5b3163 100644 --- a/src/components/Order/PaymentProcess.tsx +++ b/src/components/Order/PaymentProcess.tsx @@ -27,31 +27,13 @@ type Props = { couponDiscount: number; }; -const PaymentProcess: React.FC = ({ order, couponDiscount }) => { +const PaymentProcess: React.FC = ({ order }) => { const { token } = useAuth(); const totalProducts = order.details.reduce( (acc, item) => acc + item.quantity, 0, ); - const subtotal = order.details.reduce( - (acc, item) => acc + item.productPresentation.price * item.quantity, - 0, - ); - const itemDiscount = order.details.reduce( - (acc, item) => - acc + - (item.productPresentation.promo?.discount - ? item.productPresentation.price * - item.quantity * - (item.productPresentation.promo.discount / 100) - : 0), - 0, - ); - const tax = (subtotal - itemDiscount - couponDiscount) * 0.16; - const totalAmount = - subtotal - itemDiscount - couponDiscount + (tax > 0 ? tax : 0); - const [banks, setBanks] = useState([]); const [selectedBank, setSelectedBank] = useState(''); const [paymentConfirmation, setPaymentConfirmationData] = @@ -108,7 +90,7 @@ const PaymentProcess: React.FC = ({ order, couponDiscount }) => { [paymentConfirmation, order.id, token], ); - const isBank = false; //paymentMethod === 'bank'; + const isBank = order.paymentMethod === PaymentMethod.BANK_TRANSFER; const description = isBank ? 'Debes hacer el pago del monto exacto, la orden se creará cuando se confirme el pago' @@ -186,7 +168,7 @@ const PaymentProcess: React.FC = ({ order, couponDiscount }) => {

    Monto

    - Bs.{totalAmount.toFixed(2)} + Bs.{order.totalPrice.toFixed(2)}
    diff --git a/src/components/User/Order/UserOrderDetail.tsx b/src/components/User/Order/UserOrderDetail.tsx index f00b4ab..d14512b 100644 --- a/src/components/User/Order/UserOrderDetail.tsx +++ b/src/components/User/Order/UserOrderDetail.tsx @@ -47,7 +47,7 @@ export default function UserOrderDetail({ }, 0); return ( -
    +
    {/* Header */}

    @@ -170,8 +170,11 @@ export default function UserOrderDetail({

    {product.name}

    -

    - {product.description} +

    + {product.description?.substring(0, 50)} + {product.description && product.description.length > 50 && ( + ... + )}

    Date: Sun, 25 May 2025 19:53:04 -0400 Subject: [PATCH 42/50] Update dependencies --- package-lock.json | 65 ++++++++++++++++++++++++++++++++++------------- package.json | 1 + 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index c7f30e7..59d4245 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@heroicons/react": "^2.2.0", "@microsoft/fetch-event-source": "^2.0.1", "@next/third-parties": "^15.3.0", + "@pharmatech/sdk": "^0.4.19", "@radix-ui/react-slider": "^1.2.4", "@react-google-maps/api": "^2.20.6", "@types/google.maps": "^3.58.1", @@ -1926,6 +1927,15 @@ "node": ">=12.4.0" } }, + "node_modules/@pharmatech/sdk": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.19.tgz", + "integrity": "sha512-ZiYsoiVRtDjxs5eDqtUAZrEsUWMEZ9+q5XpSOSHt+gTcZLBzntIOO21ZyAm6ONXyRO6EORsOy2vlnnOv87Cs0g==", + "license": "MIT", + "dependencies": { + "axios": "^1.8.1" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3538,7 +3548,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/available-typed-arrays": { @@ -3567,6 +3576,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3695,7 +3715,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3980,7 +3999,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -4231,7 +4249,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -4293,7 +4310,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4493,7 +4509,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4503,7 +4518,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4548,7 +4562,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4561,7 +4574,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5279,6 +5291,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -5315,7 +5347,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5408,7 +5439,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5433,7 +5463,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5584,7 +5613,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5663,7 +5691,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5676,7 +5703,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -6859,7 +6885,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6898,7 +6923,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6908,7 +6932,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -10272,6 +10295,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index c48ceef..ed6debf 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@heroicons/react": "^2.2.0", "@microsoft/fetch-event-source": "^2.0.1", "@next/third-parties": "^15.3.0", + "@pharmatech/sdk": "^0.4.19", "@radix-ui/react-slider": "^1.2.4", "@react-google-maps/api": "^2.20.6", "@types/google.maps": "^3.58.1", From ae42df49aa1309b2edd0bd52594b229d11171ace Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 25 May 2025 20:08:59 -0400 Subject: [PATCH 43/50] Install version 0.4.20 of sdk --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59d4245..804e55c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@heroicons/react": "^2.2.0", "@microsoft/fetch-event-source": "^2.0.1", "@next/third-parties": "^15.3.0", - "@pharmatech/sdk": "^0.4.19", + "@pharmatech/sdk": "^0.4.20", "@radix-ui/react-slider": "^1.2.4", "@react-google-maps/api": "^2.20.6", "@types/google.maps": "^3.58.1", @@ -1928,9 +1928,9 @@ } }, "node_modules/@pharmatech/sdk": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.19.tgz", - "integrity": "sha512-ZiYsoiVRtDjxs5eDqtUAZrEsUWMEZ9+q5XpSOSHt+gTcZLBzntIOO21ZyAm6ONXyRO6EORsOy2vlnnOv87Cs0g==", + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.20.tgz", + "integrity": "sha512-unhWNGoPDVmZ4z877xjH5DHA6E8MYuFe6smo6JDmxFX/AbdXa3O8hTZzSAG1mc5PUmfVTa6mKeSxJpEe05fnSw==", "license": "MIT", "dependencies": { "axios": "^1.8.1" diff --git a/package.json b/package.json index ed6debf..fed1d04 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@heroicons/react": "^2.2.0", "@microsoft/fetch-event-source": "^2.0.1", "@next/third-parties": "^15.3.0", - "@pharmatech/sdk": "^0.4.19", + "@pharmatech/sdk": "^0.4.20", "@radix-ui/react-slider": "^1.2.4", "@react-google-maps/api": "^2.20.6", "@types/google.maps": "^3.58.1", From 3d3619b78284626f6cf8d96bb196e68520bf2e8c Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 25 May 2025 20:36:57 -0400 Subject: [PATCH 44/50] Format price to decimals --- .../presentation/[presentationId]/page.tsx | 3 +- src/components/Cart/CartItem.tsx | 23 +++---------- src/components/Cart/CartSummary.tsx | 7 ++-- src/components/Order/OrderSummary.tsx | 17 +++++----- src/components/Order/PaymentProcess.tsx | 3 +- src/components/Order/ProductOrderSummary.tsx | 15 ++++---- src/components/Product/ProductCard.tsx | 7 ++-- src/components/User/Order/UserOrderDetail.tsx | 34 +++++++------------ src/components/User/Order/UserOrdertable.tsx | 5 ++- src/lib/utils/helpers/priceFormatter.ts | 5 +++ 10 files changed, 54 insertions(+), 65 deletions(-) create mode 100644 src/lib/utils/helpers/priceFormatter.ts diff --git a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx index ee91f5f..6ab0bd6 100644 --- a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx +++ b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx @@ -20,6 +20,7 @@ import { } from '@pharmatech/sdk'; import Loading from '@/app/loading'; import ProductNotFound from '@/components/Product/NotFound'; +import { formatPrice } from '@/lib/utils/helpers/priceFormatter'; export default function ProductDetailPage() { const router = useRouter(); @@ -171,7 +172,7 @@ export default function ProductDetailPage() {

    - ${presentation.price.toFixed(2)} + ${formatPrice(presentation.price)}

    = ({

    {item.name}

    -

    - (${Number.isInteger(item.price) ? item.price : item.price.toFixed(2)}{' '} - c/u) -

    +

    (${formatPrice(item.price)} c/u)

    = ({ {discount > 0 ? ( <> - $ - {Number.isInteger(discountedTotal) - ? discountedTotal - : discountedTotal.toFixed(2)} + ${formatPrice(discountedTotal)} - $ - {Number.isInteger(originalTotal) - ? originalTotal - : originalTotal.toFixed(2)} + ${formatPrice(originalTotal)} ) : ( - - $ - {Number.isInteger(originalTotal) - ? originalTotal - : originalTotal.toFixed(2)} - + ${formatPrice(originalTotal)} )}
    @@ -29,7 +30,7 @@ const CartSummary: React.FC = ({ Descuento - - ${Number.isInteger(discount) ? discount : discount.toFixed(2)} + - ${formatPrice(discount)}
    @@ -37,7 +38,7 @@ const CartSummary: React.FC = ({ Total - ${Number.isInteger(total) ? total : total.toFixed(2)} + ${formatPrice(total)}
    diff --git a/src/components/Order/ProductOrderSummary.tsx b/src/components/Order/ProductOrderSummary.tsx index bc65dc5..b250c77 100644 --- a/src/components/Order/ProductOrderSummary.tsx +++ b/src/components/Order/ProductOrderSummary.tsx @@ -3,6 +3,7 @@ import React, { useMemo } from 'react'; import Image from 'next/image'; import { OrderDetailedResponse } from '@pharmatech/sdk'; +import { formatPrice } from '@/lib/utils/helpers/priceFormatter'; interface ProductOrderSummaryProps { order: OrderDetailedResponse; @@ -64,22 +65,22 @@ const ProductOrderSummary: React.FC = ({ order }) => { {detail.productPresentation.product.name}

    - ${discountedUnit.toFixed(2)} x {qty} + ${formatPrice(discountedUnit)} x {qty}

    {promo > 0 ? (

    - ${lineSubtotal.toFixed(2)} + ${formatPrice(lineSubtotal)}

    - ${(price * qty).toFixed(2)} + ${formatPrice(price * qty)}

    ) : (

    - ${lineSubtotal.toFixed(2)} + ${formatPrice(lineSubtotal)}

    )}
    @@ -91,17 +92,17 @@ const ProductOrderSummary: React.FC = ({ order }) => {
    Subtotal - ${subtotal.toFixed(2)} + ${formatPrice(subtotal)}
    {itemDiscount > 0 && (
    Descuento - -${itemDiscount.toFixed(2)} + -${formatPrice(itemDiscount)}
    )}
    Total - ${total.toFixed(2)} + ${formatPrice(total)}
    diff --git a/src/components/Product/ProductCard.tsx b/src/components/Product/ProductCard.tsx index 7b8ab79..68dbb68 100644 --- a/src/components/Product/ProductCard.tsx +++ b/src/components/Product/ProductCard.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import CardButton from '../CardButton'; import Badge from '../Badge'; import { ProductPresentation } from '@pharmatech/sdk'; +import { formatPrice } from '@/lib/utils/helpers/priceFormatter'; type Props = { product: ProductPresentation; @@ -73,7 +74,7 @@ export default function ProductCard({ product }: Props) { <>
    - ${price.toFixed(2)} + ${formatPrice(price)}
    - ${finalPrice.toFixed(2)} + ${formatPrice(finalPrice)} ) : ( - ${price.toFixed(2)} + ${formatPrice(price)} )}
    diff --git a/src/components/User/Order/UserOrderDetail.tsx b/src/components/User/Order/UserOrderDetail.tsx index d14512b..f19521f 100644 --- a/src/components/User/Order/UserOrderDetail.tsx +++ b/src/components/User/Order/UserOrderDetail.tsx @@ -9,6 +9,7 @@ import { OrderDetailProductPresentationResponse, } from '@pharmatech/sdk'; import Button from '@/components/Button'; +import { formatPrice } from '@/lib/utils/helpers/priceFormatter'; interface OrderDetailProps { orderNumber: string; @@ -76,12 +77,7 @@ export default function UserOrderDetail({ const quantity = item.quantity; const promo = presentation.promo; - const isPromoActive = - promo && - new Date(promo.startAt) <= now && - now < new Date(promo.expiredAt); - - const discountedPrice = isPromoActive + const discountedPrice = promo ? basePrice * (1 - promo.discount / 100) : basePrice; @@ -119,13 +115,11 @@ export default function UserOrderDetail({
    - {isPromoActive && ( - - ${(basePrice * quantity).toFixed(2)} - - )} + + ${formatPrice(basePrice * quantity)} + - ${(discountedPrice * quantity).toFixed(2)} + ${formatPrice(discountedPrice * quantity)}
    @@ -191,12 +185,10 @@ export default function UserOrderDetail({
    {quantity}
    - {isPromoActive && ( - - ${(basePrice * quantity).toFixed(2)} - - )} - ${(discountedPrice * quantity).toFixed(2)} + + ${formatPrice(basePrice * quantity)} + + ${formatPrice(discountedPrice * quantity)}
    @@ -215,19 +207,19 @@ export default function UserOrderDetail({ ({products.length} productos)
    -
    ${subtotal.toFixed(2)}
    +
    ${formatPrice(subtotal)}

    Descuentos
    -
    -${totalDiscount.toFixed(2)}
    +
    -${formatPrice(totalDiscount)}
    TOTAL
    -
    ${total.toFixed(2)}
    +
    ${formatPrice(total)}
    diff --git a/src/components/User/Order/UserOrdertable.tsx b/src/components/User/Order/UserOrdertable.tsx index 2f0c3a7..5ebd3ee 100644 --- a/src/components/User/Order/UserOrdertable.tsx +++ b/src/components/User/Order/UserOrdertable.tsx @@ -5,6 +5,7 @@ import type { BadgeProps } from '@/components/Badge'; import Button from '@/components/Button'; import Pagination from '@/components/Pagination'; import { OrderResponse, OrderStatus } from '@pharmatech/sdk'; +import { formatPrice } from '@/lib/utils/helpers/priceFormatter'; interface OrderTableProps { orders: OrderResponse[]; @@ -35,8 +36,6 @@ const getBadgeProps = ( } }; -const formatPrice = (price: number) => `$${price.toFixed(2)}`; - export default function OrderTable({ orders, onViewDetails }: OrderTableProps) { const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 4; @@ -57,7 +56,7 @@ export default function OrderTable({ orders, onViewDetails }: OrderTableProps) {
    #{order.id.slice(0, 8)}
    -
    {formatPrice(order.totalPrice)}
    +
    ${formatPrice(order.totalPrice)}
    {new Date(order.createdAt).toLocaleDateString('es-Es')} diff --git a/src/lib/utils/helpers/priceFormatter.ts b/src/lib/utils/helpers/priceFormatter.ts new file mode 100644 index 0000000..0040a86 --- /dev/null +++ b/src/lib/utils/helpers/priceFormatter.ts @@ -0,0 +1,5 @@ +export function formatPrice(price: number): string { + if (isNaN(price) || price < 0) return '0.00'; + + return (price / 100).toFixed(2); +} From 67e51ae8ee0e6af8c07cb5934217ded8515c858f Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 25 May 2025 20:40:26 -0400 Subject: [PATCH 45/50] Standarize currency to USD instead of VES --- src/components/Order/PaymentProcess.tsx | 2 +- src/components/SidebarFilter.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Order/PaymentProcess.tsx b/src/components/Order/PaymentProcess.tsx index a3ba0c3..bc54e71 100644 --- a/src/components/Order/PaymentProcess.tsx +++ b/src/components/Order/PaymentProcess.tsx @@ -169,7 +169,7 @@ const PaymentProcess: React.FC = ({ order }) => {

    Monto

    - Bs.{formatPrice(order.totalPrice)} + ${formatPrice(order.totalPrice)}
    diff --git a/src/components/SidebarFilter.tsx b/src/components/SidebarFilter.tsx index 4575535..0e99248 100644 --- a/src/components/SidebarFilter.tsx +++ b/src/components/SidebarFilter.tsx @@ -234,8 +234,8 @@ export default function SidebarFilter({
    - Bs {priceRange[0] / 100} - Bs {priceRange[1] / 100} + ${priceRange[0] / 100} + ${priceRange[1] / 100}
    From 01e329b812617e454ed1dfb176f0c57dc83d94ba Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Mon, 26 May 2025 17:13:27 -0400 Subject: [PATCH 46/50] Refactor to use server components --- src/app/(shop)/layout.tsx | 5 +- src/app/(shop)/page.tsx | 153 ++-------------- .../presentation/[presentationId]/page.tsx | 172 +++++------------- src/app/(shop)/product/error.tsx | 45 +++++ src/components/Home/Categories.tsx | 26 +++ src/components/Home/EmailConfirmation.tsx | 60 ++++++ src/components/Home/ProductsOffer.tsx | 16 ++ src/components/Home/ProductsRecommended.tsx | 52 ++++++ .../Product/PresentationDropdown.tsx | 26 +++ .../Product/ProductCarouselSkelete.tsx | 25 +++ 10 files changed, 315 insertions(+), 265 deletions(-) create mode 100644 src/app/(shop)/product/error.tsx create mode 100644 src/components/Home/Categories.tsx create mode 100644 src/components/Home/EmailConfirmation.tsx create mode 100644 src/components/Home/ProductsOffer.tsx create mode 100644 src/components/Home/ProductsRecommended.tsx create mode 100644 src/components/Product/PresentationDropdown.tsx create mode 100644 src/components/Product/ProductCarouselSkelete.tsx diff --git a/src/app/(shop)/layout.tsx b/src/app/(shop)/layout.tsx index 353a291..887c460 100644 --- a/src/app/(shop)/layout.tsx +++ b/src/app/(shop)/layout.tsx @@ -1,9 +1,10 @@ 'use client'; -import { ReactNode } from 'react'; +import { ReactNode, Suspense } from 'react'; import NavBar from '@/components/Navbar'; import Footer from '@/components/Footer'; import { useAuth } from '@/context/AuthContext'; +import Loading from '../loading'; type ShopLayoutProps = { children: ReactNode; @@ -21,7 +22,7 @@ export default function ShopLayout({ children }: ShopLayoutProps) {
    - {children} + }>{children}
    diff --git a/src/app/(shop)/page.tsx b/src/app/(shop)/page.tsx index 0914498..de2c47b 100644 --- a/src/app/(shop)/page.tsx +++ b/src/app/(shop)/page.tsx @@ -1,127 +1,22 @@ -'use client'; -import { useEffect, useRef, useState } from 'react'; import Carousel from '@/components/Carousel'; -import ProductCarousel from '@/components/Product/ProductCarousel'; -import { api } from '@/lib/sdkConfig'; import Banner1 from '@/lib/utils/images/banner-v2.jpg'; import Banner2 from '@/lib/utils/images/banner-v1.jpg'; import Banner3 from '@/lib/utils/images/banner_final.jpg'; -import { toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; -import ProductDetailImg from '@/lib/utils/images/Antibioticos.png'; - -import type { - Category as SDKCategory, - ProductPresentation, -} from '@pharmatech/sdk'; - -import CategoryCarousel from '@/components/CategoryCarousel'; -import EnterCodeFormModal from '@/components/EmailValidation'; -import { useAuth } from '@/context/AuthContext'; - -type CategoryForCarousel = SDKCategory & { - id: string; - imageUrl?: string; -}; - -export default function Home() { - const { token, user } = useAuth(); - const toastDisplayed = useRef(false); - const toastId = useRef(null); - - const [products, setProducts] = useState([]); - const [recommendedProducts, setRecommendedProducts] = useState< - ProductPresentation[] - >([]); - const [categories, setCategories] = useState([]); - const [showEmailModal, setShowEmailModal] = useState(false); - +import ProductCarouselSkeleton from '@/components/Product/ProductCarouselSkelete'; +import EmailConfirmation from '@/components/Home/EmailConfirmation'; +import { Suspense } from 'react'; +import ProductsRecommended from '@/components/Home/ProductsRecommended'; +import ProductsOffer from '@/components/Home/ProductsOffer'; +import Categories from '@/components/Home/Categories'; + +export default async function Home() { const slides = [ { id: 1, imageUrl: Banner1 }, { id: 2, imageUrl: Banner2 }, { id: 3, imageUrl: Banner3 }, ]; - useEffect(() => { - const checkUserValidation = async () => { - if (!token || !user?.sub) return; - try { - const profile = await api.user.getProfile(user.sub, token); - if (!profile.isValidated && !toastDisplayed.current) { - toastId.current = toast.info( -
    - Verifica tu correo electrónico.{' '} - -
    , - { - autoClose: false, - closeOnClick: false, - draggable: true, - position: 'top-right', - }, - ); - toastDisplayed.current = true; - } - } catch (err) { - console.error('Error verificando validación del usuario:', err); - } - }; - checkUserValidation(); - }, [token, user]); - - useEffect(() => { - const fetchProducts = async () => { - try { - const data = await api.product.getProducts({ page: 1, limit: 20 }); - setProducts(data.results); - } catch (err) { - console.error('Error fetching products:', err); - } - }; - fetchProducts(); - }, []); - - useEffect(() => { - const fetchCategories = async () => { - try { - const resp = await api.category.findAll({ page: 1, limit: 20 }); - const formatted: CategoryForCarousel[] = resp.results.map((c) => ({ - ...c, - id: String(c.id), - imageUrl: ProductDetailImg.src, - })); - setCategories(formatted); - } catch (err) { - console.error('Error fetching categories:', err); - } - }; - fetchCategories(); - }, []); - - useEffect(() => { - const fetchRecommended = async () => { - if (!token || !user?.sub) { - setRecommendedProducts([]); - return; - } - try { - const data = await api.product.getRecommendations(token); - setRecommendedProducts(data.results); - } catch (err) { - console.error('Error fetching recommended products:', err); - } - }; - fetchRecommended(); - }, [token, user]); - return (

    Pharmatech

    @@ -134,38 +29,22 @@ export default function Home() {

    Productos en Oferta Exclusiva

    -
    - -
    + }> + + {/* Sección recomendados */} - {recommendedProducts.length > 0 && ( - <> -

    - Productos Recomendados para ti -

    -
    - -
    - - )} + {/* Sección categorías */}

    Categorías

    -
    - -
    + }> + +
    {/* Modal de verificación de email */} - {token && user?.sub && ( - setShowEmailModal(false)} - userId={user.sub} - jwt={token} - /> - )} +
    ); } diff --git a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx index 6ab0bd6..b93eb00 100644 --- a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx +++ b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx @@ -1,136 +1,62 @@ -'use client'; -import React, { useState, useEffect } from 'react'; -import { useRouter, useParams, useSearchParams } from 'next/navigation'; import Breadcrumb from '@/components/Breadcrumb'; import Badge from '@/components/Badge'; -import Carousel, { Slide } from '@/components/Product/Carousel'; -import Dropdown from '@/components/Dropdown'; +import Carousel from '@/components/Product/Carousel'; import CardButton from '@/components/CardButton'; import ProductBranch from '@/components/Product/ProductBranch'; import ProductCarousel from '@/components/Product/ProductCarousel'; import { StarIcon } from '@heroicons/react/24/solid'; import { Colors } from '@/styles/styles'; import { api } from '@/lib/sdkConfig'; -import { - ProductPresentationDetailResponse, - GenericProductResponse, - ProductPresentationResponse, - ProductPresentation, - ProductPaginationRequest, -} from '@pharmatech/sdk'; -import Loading from '@/app/loading'; -import ProductNotFound from '@/components/Product/NotFound'; import { formatPrice } from '@/lib/utils/helpers/priceFormatter'; +import PresentationDropdown from '@/components/Product/PresentationDropdown'; + +export default async function ProductDetailPage({ + params, +}: { + params: Promise<{ productId: string; presentationId: string }>; +}) { + const { productId, presentationId } = await params; + + const presentation = await api.productPresentation.getByPresentationId( + productId, + presentationId, + ); -export default function ProductDetailPage() { - const router = useRouter(); - const params = useParams(); - const searchParams = useSearchParams(); - - // Detect if we came from a filtered search - const queryString = searchParams?.toString() || ''; - const isCustomSearch = queryString.length > 0; - - const productId = String(params?.productId || ''); - const presentationId = String(params?.presentationId || ''); - - const [presentation, setPresentation] = - useState(); - const [genericProduct, setGenericProduct] = - useState(); - const [slides, setSlides] = useState([]); - const [products, setProducts] = useState([]); - const [presentationList, setPresentationList] = useState< - ProductPresentationResponse[] - >([]); - const [presentationIsLoading, setPresentationIsLoading] = useState(true); - const [productIsLoading, setProductIsLoading] = useState(true); - - // 1) Load presentation detail - useEffect(() => { - if (!presentationId) return; - api.productPresentation - .getByPresentationId(productId, presentationId) - .then((data) => setPresentation(data)) - .catch((err) => { - console.error(err); - setProductIsLoading(false); - }) - .finally(() => setPresentationIsLoading(false)); - }, [productId, presentationId]); - - // 2) Load generic product info & variants - useEffect(() => { - if (!presentation) return; - api.genericProduct - .getById(productId) - .then((data) => { - setGenericProduct(data); - return api.productPresentation.getByProductId(productId); - }) - .then((list) => setPresentationList(list)) - .catch((err) => console.error(err)); - }, [presentation, productId]); + const genericProduct = await api.genericProduct.getById(productId); - // 3) Load images - useEffect(() => { - if (!genericProduct) return; - api.productImage - .getByProductId(genericProduct.id) - .then((imgs) => { - if (imgs.length) { - setSlides(imgs.map((img, i) => ({ id: i, imageUrl: img.url }))); - } else { - setSlides([ + const slides = await api.productImage + .getByProductId(genericProduct.id) + .then((imgs) => + imgs.length + ? imgs.map((img, i) => ({ id: i, imageUrl: img.url })) + : [ { id: 1, imageUrl: '/images/product-detail.jpg' }, { id: 2, imageUrl: '/images/product-detail-2.jpg' }, - ]); - } - }) - .catch(() => - setSlides([ - { id: 1, imageUrl: '/images/product-detail.jpg' }, - { id: 2, imageUrl: '/images/product-detail-2.jpg' }, - ]), - ) - .finally(() => { - setProductIsLoading(false); - }); - }, [genericProduct]); - - // 4) Fetch related products - useEffect(() => { - if (!genericProduct) return; - const req: ProductPaginationRequest = { - page: 1, - limit: 20, - ...(genericProduct.manufacturer.id && { - manufacturerId: [genericProduct.manufacturer.id], - }), - }; - api.product - .getProducts(req) - .then((res) => setProducts(res.results)) - .catch((err) => console.error(err)); - }, [genericProduct]); - - if (presentationIsLoading || productIsLoading) { - return ; - } - - if (!presentation || !genericProduct) { - return ; - } + ], + ) + .catch(() => [ + { id: 1, imageUrl: '/images/product-detail.jpg' }, + { id: 2, imageUrl: '/images/product-detail-2.jpg' }, + ]); + + const presentationList = + await api.productPresentation.getByProductId(productId); + + const relatedProducts = await api.product.getProducts({ + page: 1, + limit: 20, + ...(genericProduct.manufacturer.id && { + manufacturerId: [genericProduct.manufacturer.id], + }), + }); // Breadcrumb con acción de "volver" si es búsqueda personalizada const breadcrumbItems = [ { label: 'Inicio', href: '/' }, - isCustomSearch - ? { label: 'Búsqueda personalizada', onClick: () => router.back() } - : { - label: genericProduct.categories?.[0]?.name ?? 'Categoría', - href: `/search?category=${genericProduct.categories?.[0]?.name}`, - }, + { + label: genericProduct.categories?.[0]?.name ?? 'Categoría', + href: `/search?category=${genericProduct.categories?.[0]?.name}`, + }, { label: presentation.presentation.name }, ]; @@ -139,11 +65,6 @@ export default function ProductDetailPage() { display: `${genericProduct.genericName} ${item.presentation.name} ${item.presentation.quantity} ${item.presentation.measurementUnit}`, })); - const handlePresentationSelect = (display: string) => { - const found = variantOptions.find((v) => v.display === display); - if (found) router.push(`/product/${productId}/presentation/${found.id}`); - }; - return (
    @@ -184,10 +105,9 @@ export default function ProductDetailPage() { }} />
    - v.display)} - onSelect={handlePresentationSelect} +
    @@ -200,7 +120,7 @@ export default function ProductDetailPage() {

    Productos de la marca {genericProduct.manufacturer.name}

    - p)} /> + p)} />
    ); diff --git a/src/app/(shop)/product/error.tsx b/src/app/(shop)/product/error.tsx new file mode 100644 index 0000000..c794489 --- /dev/null +++ b/src/app/(shop)/product/error.tsx @@ -0,0 +1,45 @@ +'use client'; + +import React from 'react'; +import { useRouter } from 'next/navigation'; +import { Colors, FontSizes } from '@/styles/styles'; +import Button from '@/components/Button'; + +export default function ProductNotFound() { + const router = useRouter(); + + const handleGoBack = () => { + router.back(); + }; + + return ( +
    + {/* Title */} +

    + Producto no encontrado +

    + + {/* Description */} +

    + Lo sentimos, no pudimos encontrar el producto que estás buscando. +

    + + {/* Go Back Button */} + +
    + ); +} diff --git a/src/components/Home/Categories.tsx b/src/components/Home/Categories.tsx new file mode 100644 index 0000000..ed5a7a0 --- /dev/null +++ b/src/components/Home/Categories.tsx @@ -0,0 +1,26 @@ +import { api } from '@/lib/sdkConfig'; +import CategoryCarousel from '../CategoryCarousel'; +import { Category } from '@pharmatech/sdk'; +import ProductDetailImg from '@/lib/utils/images/Antibioticos.png'; + +type CategoryForCarousel = Category & { + id: string; + imageUrl?: string; +}; + +export default async function Categories() { + const response = await api.category.findAll({ + page: 1, + limit: 20, + }); + const categories: CategoryForCarousel[] = response.results.map((c) => ({ + ...c, + id: String(c.id), + imageUrl: ProductDetailImg.src, + })); + return ( +
    + +
    + ); +} diff --git a/src/components/Home/EmailConfirmation.tsx b/src/components/Home/EmailConfirmation.tsx new file mode 100644 index 0000000..b8c38b9 --- /dev/null +++ b/src/components/Home/EmailConfirmation.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { useAuth } from '@/context/AuthContext'; +import EnterCodeFormModal from '../EmailValidation'; +import { useEffect, useRef, useState } from 'react'; +import { api } from '@/lib/sdkConfig'; +import { toast } from 'react-toastify'; + +export default function EmailConfirmation() { + const { token, user } = useAuth(); + const [showEmailModal, setShowEmailModal] = useState(false); + const toastDisplayed = useRef(false); + const toastId = useRef(null); + useEffect(() => { + const checkUserValidation = async () => { + if (!token || !user?.sub) return; + try { + const profile = await api.user.getProfile(user.sub, token); + if (!profile.isValidated && !toastDisplayed.current) { + toastId.current = toast.info( +
    + Verifica tu correo electrónico.{' '} + +
    , + { + autoClose: false, + closeOnClick: false, + draggable: true, + position: 'top-right', + }, + ); + toastDisplayed.current = true; + } + } catch (err) { + console.error('Error verificando validación del usuario:', err); + } + }; + checkUserValidation(); + }, [token, user]); + return ( + <> + {token && user?.sub && ( + setShowEmailModal(false)} + userId={user.sub} + jwt={token} + /> + )} + + ); +} diff --git a/src/components/Home/ProductsOffer.tsx b/src/components/Home/ProductsOffer.tsx new file mode 100644 index 0000000..22143bd --- /dev/null +++ b/src/components/Home/ProductsOffer.tsx @@ -0,0 +1,16 @@ +import ProductCarousel from '@/components/Product/ProductCarousel'; +import { api } from '@/lib/sdkConfig'; + +export default async function ProductsOffer() { + const products = await api.product.getProducts({ + page: 1, + limit: 20, + }); + return ( + <> +
    + +
    + + ); +} diff --git a/src/components/Home/ProductsRecommended.tsx b/src/components/Home/ProductsRecommended.tsx new file mode 100644 index 0000000..30e77f2 --- /dev/null +++ b/src/components/Home/ProductsRecommended.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useAuth } from '@/context/AuthContext'; +import { api } from '@/lib/sdkConfig'; +import { ProductPresentation } from '@pharmatech/sdk'; +import { useEffect, useState } from 'react'; +import ProductCarousel from '../Product/ProductCarousel'; +import ProductCarouselSkeleton from '../Product/ProductCarouselSkelete'; + +export default function ProductsRecommended() { + const { token, user } = useAuth(); + const [recommendedProducts, setRecommendedProducts] = useState< + ProductPresentation[] + >([]); + const [isLoading, setIsLoading] = useState(false); + useEffect(() => { + const fetchRecommended = async () => { + if (!token || !user?.sub) { + setRecommendedProducts([]); + return; + } + setIsLoading(true); + try { + const data = await api.product.getRecommendations(token); + setRecommendedProducts(data.results); + } catch (err) { + console.error('Error fetching recommended products:', err); + } finally { + setIsLoading(false); + } + }; + fetchRecommended(); + }, [token, user]); + return ( + <> + {token && user && ( + <> +

    + Productos Recomendados para ti +

    +
    + {isLoading ? ( + + ) : ( + + )} +
    + + )} + + ); +} diff --git a/src/components/Product/PresentationDropdown.tsx b/src/components/Product/PresentationDropdown.tsx new file mode 100644 index 0000000..956dca7 --- /dev/null +++ b/src/components/Product/PresentationDropdown.tsx @@ -0,0 +1,26 @@ +'use client'; + +import Dropdown from '../Dropdown'; +import { useRouter } from 'next/navigation'; + +export default function PresentationDropdown({ + productId, + options, +}: { + productId: string; + options: { id: string; display: string }[]; +}) { + const router = useRouter(); + const handlePresentationSelect = (display: string) => { + const found = options.find((v) => v.display === display); + if (found) router.push(`/product/${productId}/presentation/${found.id}`); + }; + + return ( + v.display)} + onSelect={handlePresentationSelect} + /> + ); +} diff --git a/src/components/Product/ProductCarouselSkelete.tsx b/src/components/Product/ProductCarouselSkelete.tsx new file mode 100644 index 0000000..aa9466b --- /dev/null +++ b/src/components/Product/ProductCarouselSkelete.tsx @@ -0,0 +1,25 @@ +import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/solid'; + +export default function ProductCarouselSkeleton() { + return ( +
    + + +
    +
    + {Array.from({ length: 4 }).map((_, index) => ( +
    + ))} +
    +
    + +
    + ); +} From ca34de82d5861447fe6d99fff19d673ad8df0a6e Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Mon, 26 May 2025 17:15:23 -0400 Subject: [PATCH 47/50] Update tests --- __tests__/page.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/page.test.tsx b/__tests__/page.test.tsx index 93ac530..65b18ec 100644 --- a/__tests__/page.test.tsx +++ b/__tests__/page.test.tsx @@ -4,8 +4,8 @@ import Home from '@/app/(shop)/page'; import { describe, it, expect } from 'vitest'; describe('Home Page', () => { - it('displays the main heading', () => { - render(); + it('displays the main heading', async () => { + render(await Home()); const heading = screen.getByText('Pharmatech'); expect(heading).toBeTruthy(); // Check if the element exists }); From 53b51c9197901b04d02d69f77c06a631e33f429e Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 29 May 2025 10:42:21 -0400 Subject: [PATCH 48/50] Update sdk version to 0.4.21 --- package-lock.json | 8 +- package.json | 2 +- .../(shop)/user/order/[id]/detail/page.tsx | 51 ++--------- src/components/Order/ProductOrderSummary.tsx | 10 +-- src/components/User/Order/UserOrderDetail.tsx | 85 +++++++------------ 5 files changed, 45 insertions(+), 111 deletions(-) diff --git a/package-lock.json b/package-lock.json index 804e55c..1452240 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@heroicons/react": "^2.2.0", "@microsoft/fetch-event-source": "^2.0.1", "@next/third-parties": "^15.3.0", - "@pharmatech/sdk": "^0.4.20", + "@pharmatech/sdk": "^0.4.21", "@radix-ui/react-slider": "^1.2.4", "@react-google-maps/api": "^2.20.6", "@types/google.maps": "^3.58.1", @@ -1928,9 +1928,9 @@ } }, "node_modules/@pharmatech/sdk": { - "version": "0.4.20", - "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.20.tgz", - "integrity": "sha512-unhWNGoPDVmZ4z877xjH5DHA6E8MYuFe6smo6JDmxFX/AbdXa3O8hTZzSAG1mc5PUmfVTa6mKeSxJpEe05fnSw==", + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.21.tgz", + "integrity": "sha512-fxyXlgKN3qLxuGVg6bmRS5DhqfLOLsCYazffax5x0iYFFBIQHQBVRLltm5h84jvOEM0oA7ipD08VVXWMyBj/vQ==", "license": "MIT", "dependencies": { "axios": "^1.8.1" diff --git a/package.json b/package.json index fed1d04..3f2487d 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@heroicons/react": "^2.2.0", "@microsoft/fetch-event-source": "^2.0.1", "@next/third-parties": "^15.3.0", - "@pharmatech/sdk": "^0.4.20", + "@pharmatech/sdk": "^0.4.21", "@radix-ui/react-slider": "^1.2.4", "@react-google-maps/api": "^2.20.6", "@types/google.maps": "^3.58.1", diff --git a/src/app/(shop)/user/order/[id]/detail/page.tsx b/src/app/(shop)/user/order/[id]/detail/page.tsx index 4de4164..dc66b06 100644 --- a/src/app/(shop)/user/order/[id]/detail/page.tsx +++ b/src/app/(shop)/user/order/[id]/detail/page.tsx @@ -4,29 +4,15 @@ import { useParams } from 'next/navigation'; import { useEffect, useState } from 'react'; import UserOrderDetail from '@/components/User/Order/UserOrderDetail'; import { api } from '@/lib/sdkConfig'; -import { OrderResponse, OrderDetailResponse } from '@pharmatech/sdk'; +import { OrderDetailedResponse } from '@pharmatech/sdk'; import { useAuth } from '@/context/AuthContext'; import Loading from '@/app/loading'; -interface OrderDetailData { - orderNumber: string; - products: OrderDetailResponse[]; - subtotal: number; - discount: number; - tax: number; - total: number; -} - -// Extends de OrderRespons para incluir detalles -type ExtendedOrderResponse = OrderResponse & { - details: OrderDetailResponse[]; -}; - export default function OrderDetailPage() { const params = useParams(); const { token } = useAuth(); const id = Array.isArray(params?.id) ? params.id[0] : (params?.id ?? ''); - const [orderData, setOrderData] = useState(null); + const [orderData, setOrderData] = useState(); const [loading, setLoading] = useState(true); useEffect(() => { @@ -34,37 +20,10 @@ export default function OrderDetailPage() { if (!id || !token) return; try { - const order: ExtendedOrderResponse = await api.order.getById(id, token); - - const products: OrderDetailResponse[] = order.details; - - const subtotal = products.reduce((sum, item) => sum + item.subtotal, 0); - - const discount = products.reduce((acc, item) => { - const promo = item.productPresentation.promo; - if (promo) { - const originalPrice = - item.productPresentation.price * item.quantity; - const itemDiscount = originalPrice - item.subtotal; - return acc + itemDiscount; - } - return acc; - }, 0); - - const tax = 0; - const total = order.totalPrice; - - setOrderData({ - orderNumber: order.id, - products, - subtotal, - discount, - tax, - total, - }); + const order = await api.order.getById(id, token); + setOrderData(order); } catch (error) { console.error('Error al obtener detalles del pedido:', error); - setOrderData(null); } finally { setLoading(false); } @@ -78,7 +37,7 @@ export default function OrderDetailPage() { {loading ? ( ) : orderData ? ( - + ) : (
    Pedido no encontrado. diff --git a/src/components/Order/ProductOrderSummary.tsx b/src/components/Order/ProductOrderSummary.tsx index b250c77..6a115ae 100644 --- a/src/components/Order/ProductOrderSummary.tsx +++ b/src/components/Order/ProductOrderSummary.tsx @@ -13,11 +13,11 @@ const ProductOrderSummary: React.FC = ({ order }) => { // Compute items, subtotal and discounts const { items, subtotal, itemDiscount, total } = useMemo(() => { const items = order.details.map((detail) => { - const price = detail.productPresentation.price; + const price = detail.price; const qty = detail.quantity; - const promo = detail.productPresentation.promo?.discount || 0; + const promo = detail.discount || 0; const discountedUnit = promo > 0 ? price * (1 - promo / 100) : price; - const lineSubtotal = discountedUnit * qty; + const lineSubtotal = detail.subtotal; const lineDiscount = promo > 0 ? (price - discountedUnit) * qty : 0; return { detail, @@ -31,8 +31,8 @@ const ProductOrderSummary: React.FC = ({ order }) => { }); const subtotal = items.reduce((sum, i) => sum + i.price * i.qty, 0); - const itemDiscount = items.reduce((sum, i) => sum + i.lineDiscount, 0); - const total = subtotal - itemDiscount; + const total = order.totalPrice; + const itemDiscount = subtotal - total; return { items, subtotal, itemDiscount, total }; }, [order.details]); diff --git a/src/components/User/Order/UserOrderDetail.tsx b/src/components/User/Order/UserOrderDetail.tsx index f19521f..99abbb8 100644 --- a/src/components/User/Order/UserOrderDetail.tsx +++ b/src/components/User/Order/UserOrderDetail.tsx @@ -5,56 +5,31 @@ import Link from 'next/link'; import { StarIcon } from '@heroicons/react/24/outline'; import CheckButton from '@/components/CheckButton'; import { - OrderDetailResponse, OrderDetailProductPresentationResponse, + OrderDetailedResponse, } from '@pharmatech/sdk'; import Button from '@/components/Button'; import { formatPrice } from '@/lib/utils/helpers/priceFormatter'; -interface OrderDetailProps { - orderNumber: string; - products: OrderDetailResponse[]; - subtotal: number; - total: number; -} - export default function UserOrderDetail({ - orderNumber, - products, - subtotal, - total, -}: OrderDetailProps) { - const now = new Date(); - - const totalDiscount = products.reduce((acc, item) => { - const presentation: OrderDetailProductPresentationResponse = - item.productPresentation; - const promo = presentation.promo; - const basePrice = presentation.price; - const quantity = item.quantity; - - const isPromoActive = - promo && - new Date(promo.startAt) <= now && - now < new Date(promo.expiredAt); - - if (isPromoActive) { - const discountedPrice = basePrice * (1 - promo.discount / 100); - const discountAmount = (basePrice - discountedPrice) * quantity; - return acc + discountAmount; - } - - return acc; - }, 0); + order, +}: { + order: OrderDetailedResponse; +}) { + const subtotal = order.details.reduce( + (acc, item) => acc + item.price * item.quantity, + 0, + ); + const totalDiscount = subtotal - order.totalPrice; return (
    {/* Header */}

    - Pedido #{orderNumber.slice(0, 8)} + Pedido #{order.id.slice(0, 8)}

    - +
    @@ -204,7 +177,7 @@ export default function UserOrderDetail({
    Subtotal{' '} - ({products.length} productos) + ({order.details.length} productos)
    ${formatPrice(subtotal)}
    @@ -219,7 +192,9 @@ export default function UserOrderDetail({
    TOTAL
    -
    ${formatPrice(total)}
    +
    + ${formatPrice(order.totalPrice)} +
    From b32cc73ef095bb1fa2e5c62f0b5675bc620d1649 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 29 May 2025 15:27:26 -0400 Subject: [PATCH 49/50] Show errors when submit --- src/components/Order/PaymentProcess.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Order/PaymentProcess.tsx b/src/components/Order/PaymentProcess.tsx index bc54e71..896c786 100644 --- a/src/components/Order/PaymentProcess.tsx +++ b/src/components/Order/PaymentProcess.tsx @@ -54,7 +54,6 @@ const PaymentProcess: React.FC = ({ order }) => { ...paymentConfirmation, [field]: e.target.value, }); - setErrors({ ...errors, [field]: '' }); }; const handleSubmit = useCallback( @@ -62,7 +61,6 @@ const PaymentProcess: React.FC = ({ order }) => { e.preventDefault(); setErrors({}); - // Construimos el payload incluyendo orderId aquí const payload: PaymentConfirmation = { ...paymentConfirmation, orderId: order.id, From 0a7a99e042c99fe4d9e1710305f06b054d5a7c93 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 29 May 2025 15:40:28 -0400 Subject: [PATCH 50/50] Upgrade version to 1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3f2487d..060bc22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ecommerce", - "version": "0.4.0", + "version": "1.0.0", "private": true, "scripts": { "dev": "next dev",