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 }); diff --git a/package-lock.json b/package-lock.json index fc8fbde..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" @@ -3577,9 +3577,10 @@ } }, "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==", + "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", @@ -5300,6 +5301,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -10296,7 +10298,8 @@ "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==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/punycode": { "version": "2.3.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", diff --git a/src/app/(shop)/layout.tsx b/src/app/(shop)/layout.tsx index bb0ec5b..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; @@ -18,10 +19,10 @@ export default function ShopLayout({ children }: ShopLayoutProps) {
{/* Nav */} -
+
- {children} + }>{children}
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)/page.tsx b/src/app/(shop)/page.tsx index 936ac22..de2c47b 100644 --- a/src/app/(shop)/page.tsx +++ b/src/app/(shop)/page.tsx @@ -1,127 +1,25 @@ -'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) 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

+

Pharmatech

{/* Carrusel principal */}
@@ -131,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 8814aa9..b93eb00 100644 --- a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx +++ b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx @@ -1,122 +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 function ProductDetailPage() { - const router = useRouter(); - const params = useParams(); - const searchParams = useSearchParams(); +export default async function ProductDetailPage({ + params, +}: { + params: Promise<{ productId: string; presentationId: string }>; +}) { + const { productId, presentationId } = await params; - // 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(null); - const [genericProduct, setGenericProduct] = - useState(null); - const [slides, setSlides] = useState([]); - const [products, setProducts] = useState([]); - const [presentationList, setPresentationList] = useState< - ProductPresentationResponse[] - >([]); - const [loading, setLoading] = useState(true); - - // 1) Load presentation detail - useEffect(() => { - if (!presentationId) return; - api.productPresentation - .getByPresentationId(productId, presentationId) - .then((data) => setPresentation(data)) - .catch((err) => console.error(err)) - .finally(() => setLoading(false)); - }, [productId, presentationId]); + const presentation = await api.productPresentation.getByPresentationId( + 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' }, - ]), - ); - }, [genericProduct]); + ], + ) + .catch(() => [ + { id: 1, imageUrl: '/images/product-detail.jpg' }, + { id: 2, imageUrl: '/images/product-detail-2.jpg' }, + ]); - // 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 (loading) return ; - if (!presentation || !genericProduct) return ; + 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 }, ]; @@ -125,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 (
@@ -153,9 +88,12 @@ export default function ProductDetailPage() {

{presentation.presentation.description}

+

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

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

- v.display)} - onSelect={handlePresentationSelect} +
@@ -183,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/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/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/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/components/Avatar.tsx b/src/components/Avatar.tsx index f1ca606..3509614 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,15 @@ export default function Avatar({ const handleLogoutClick = () => { logout(); + clearCart(); setDropdownOpen(false); - router.push('/'); + }; + + const handleSafeProfileClick = () => { + if (onProfileClick) { + setDropdownOpen(false); + onProfileClick(); + } }; useEffect(() => { @@ -114,7 +123,7 @@ export default function Avatar({ {onProfileClick && (
  • Ir a mi perfil
  • diff --git a/src/components/Cart/CartItem.tsx b/src/components/Cart/CartItem.tsx index 89322e3..f636169 100644 --- a/src/components/Cart/CartItem.tsx +++ b/src/components/Cart/CartItem.tsx @@ -4,6 +4,7 @@ import Image from 'next/image'; import { CartItem } from '@/context/CartContext'; import CardButton from '../CardButton'; import { TrashIcon } from '@heroicons/react/24/outline'; +import { formatPrice } from '@/lib/utils/helpers/priceFormatter'; interface CartItemProps { item: CartItem; @@ -56,10 +57,7 @@ const CartItemComponent: React.FC = ({

    {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)}
    +
    , + { + 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/Navbar.tsx b/src/components/Navbar.tsx index e9a864d..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,8 +32,8 @@ 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,25 +42,12 @@ 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); 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); @@ -87,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'); }; @@ -114,8 +96,6 @@ export default function NavBar({ onCartClick }: NavBarProps) { /> setIsCartOpen(true)} > - - {totalCount} - + {itemsCount > 0 && ( + + {itemsCount} + + )} - - {isLoggedIn && userData ? ( - router.push('/user')} - /> + <> + + router.push('/user')} + /> + ) : showLogin ? ( + +
    +
    + {Array.from({ length: 4 }).map((_, index) => ( +
    + ))} +
    +
    + + + ); +} diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index fe2a142..907e0ed 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { ChevronDownIcon, @@ -8,11 +8,10 @@ import { MagnifyingGlassIcon, } from '@heroicons/react/24/outline'; import SearchSuggestions from './SuggestionProduct'; -import { CategoryResponse } from '@pharmatech/sdk'; +import { CategoryResponse, Pagination } from '@pharmatech/sdk'; +import { api } from '@/lib/sdkConfig'; type SearchBarProps = { - categories: CategoryResponse[]; - onSearch?: (query: string, category: string) => 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..0e99248 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]} + ${priceRange[0] / 100} + ${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} +

    diff --git a/src/components/User/Order/UserOrderDetail.tsx b/src/components/User/Order/UserOrderDetail.tsx index f00b4ab..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; @@ -47,7 +48,7 @@ export default function UserOrderDetail({ }, 0); return ( -
    +
    {/* Header */}

    @@ -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)}
    @@ -170,8 +164,11 @@ export default function UserOrderDetail({

    {product.name}

    -

    - {product.description} +

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

    {quantity}
    - {isPromoActive && ( - - ${(basePrice * quantity).toFixed(2)} - - )} - ${(discountedPrice * quantity).toFixed(2)} + + ${formatPrice(basePrice * quantity)} + + ${formatPrice(discountedPrice * quantity)}
    @@ -212,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/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..356f22c 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 { @@ -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,20 +34,8 @@ const CartContext = createContext(undefined); export const CartProvider = ({ children }: { children: ReactNode }) => { const [cartItems, setCartItems] = useState([]); + const [itemsCount, setItemsCount] = useState(0); 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 +52,22 @@ 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; - }; + useEffect(() => { + const totalItems = cartItems.reduce((acc, item) => acc + item.quantity, 0); + setItemsCount(totalItems); + }, [cartItems]); - // 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 +90,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,23 +111,28 @@ 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 ( {children} 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); +}