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 abd50b1..1452240 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "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", + "@microsoft/fetch-event-source": "^2.0.1", "@next/third-parties": "^15.3.0", - "@pharmatech/sdk": "^0.4.16", + "@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", @@ -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", @@ -1921,9 +1928,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.21", + "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.21.tgz", + "integrity": "sha512-fxyXlgKN3qLxuGVg6bmRS5DhqfLOLsCYazffax5x0iYFFBIQHQBVRLltm5h84jvOEM0oA7ipD08VVXWMyBj/vQ==", "license": "MIT", "dependencies": { "axios": "^1.8.1" @@ -3570,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", @@ -5293,6 +5301,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -10289,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 7ab554d..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", @@ -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.16", + "@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", + "cloudinary": "^2.6.0", "jwt-decode": "^4.0.0", "lucide-react": "^0.477.0", "next": "^15.3.0", diff --git a/public/icons/approved.svg b/public/icons/approved.svg new file mode 100644 index 0000000..7181391 --- /dev/null +++ b/public/icons/approved.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/delivered_icon.svg b/public/icons/delivered_icon.svg new file mode 100644 index 0000000..aa65189 --- /dev/null +++ b/public/icons/delivered_icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/delivery.svg b/public/icons/delivery.svg new file mode 100644 index 0000000..f0811a3 --- /dev/null +++ b/public/icons/delivery.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/error.svg b/public/icons/error.svg new file mode 100644 index 0000000..a71ab0b --- /dev/null +++ b/public/icons/error.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/in_update.svg b/public/icons/in_update.svg new file mode 100644 index 0000000..7d2a00b --- /dev/null +++ b/public/icons/in_update.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/pickup_icon.svg b/public/icons/pickup_icon.svg new file mode 100644 index 0000000..2de32b0 --- /dev/null +++ b/public/icons/pickup_icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/updated.png b/public/icons/updated.png new file mode 100644 index 0000000..09b2853 Binary files /dev/null and b/public/icons/updated.png differ diff --git a/src/app/(shop)/layout.tsx b/src/app/(shop)/layout.tsx index 5f23b1a..887c460 100644 --- a/src/app/(shop)/layout.tsx +++ b/src/app/(shop)/layout.tsx @@ -1,22 +1,28 @@ '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; }; export default function ShopLayout({ children }: ShopLayoutProps) { + const { isLoading } = useAuth(); + + if (isLoading) return null; + return (
{/* Nav */} -
+
- {children} + }>{children}
diff --git a/src/app/(shop)/order/[id]/page.tsx b/src/app/(shop)/order/[id]/page.tsx index 6b566b2..09632f6 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']; @@ -148,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 88556b4..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,41 +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 a2aa1b5..b93eb00 100644 --- a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx +++ b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx @@ -1,123 +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'; - -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(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)); - }, [productId, presentationId]); +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, + ); - // 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]); - - // 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)) - .finally(() => setLoading(false)); - }, [genericProduct]); - - if (loading) 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 }, ]; @@ -126,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 (
@@ -154,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} +
@@ -184,7 +120,7 @@ export default function ProductDetailPage() {

Productos de la marca {genericProduct.manufacturer.name}

- + 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 57a1c85..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'}

@@ -279,7 +159,7 @@ export default function SearchPage() {
{displayProducts.map((p) => ( - + ))}
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/(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/app/register/RegisterForm.tsx b/src/app/register/RegisterForm.tsx index d85b10b..42978af 100644 --- a/src/app/register/RegisterForm.tsx +++ b/src/app/register/RegisterForm.tsx @@ -26,25 +26,25 @@ export default function RegisterForm() { }, [token, router]); const [formData, setFormData] = useState({ - nombre: '', - apellido: '', + firstName: '', + lastName: '', email: '', - cedula: '', - telefono: '', - fechaNacimiento: '', - genero: '', + documentId: '', + phoneNumber: '', + birthDate: '', + gender: '', password: '', confirmPassword: '', }); const [errors, setErrors] = useState({ - nombre: '', - apellido: '', + firstName: '', + lastName: '', email: '', - cedula: '', - telefono: '', - fechaNacimiento: '', - genero: '', + documentId: '', + phoneNumber: '', + birthDate: '', + gender: '', password: '', confirmPassword: '', }); @@ -59,15 +59,15 @@ export default function RegisterForm() { }; const handleGenderClick = (gender: 'hombre' | 'mujer') => { - if (formData.genero !== gender) { - setFormData({ ...formData, genero: gender }); - setErrors({ ...errors, genero: '' }); + if (formData.gender !== gender) { + setFormData({ ...formData, gender: gender }); + setErrors({ ...errors, gender: '' }); } }; const handleDateSelect = (date: string) => { - setFormData({ ...formData, fechaNacimiento: date }); - setErrors({ ...errors, fechaNacimiento: '' }); + setFormData({ ...formData, birthDate: date }); + setErrors({ ...errors, birthDate: '' }); }; const handleSubmit = useCallback( @@ -80,13 +80,13 @@ export default function RegisterForm() { if (!result.success) { const { fieldErrors } = result.error.flatten(); setErrors({ - nombre: fieldErrors.nombre?.[0] ?? '', - apellido: fieldErrors.apellido?.[0] ?? '', + firstName: fieldErrors.firstName?.[0] ?? '', + lastName: fieldErrors.lastName?.[0] ?? '', email: fieldErrors.email?.[0] ?? '', - cedula: fieldErrors.cedula?.[0] ?? '', - telefono: fieldErrors.telefono?.[0] ?? '', - fechaNacimiento: fieldErrors.fechaNacimiento?.[0] ?? '', - genero: fieldErrors.genero?.[0] ?? '', + documentId: fieldErrors.documentId?.[0] ?? '', + phoneNumber: fieldErrors.phoneNumber?.[0] ?? '', + birthDate: fieldErrors.birthDate?.[0] ?? '', + gender: fieldErrors.gender?.[0] ?? '', password: fieldErrors.password?.[0] ?? '', confirmPassword: fieldErrors.confirmPassword?.[0] ?? '', }); @@ -101,20 +101,21 @@ export default function RegisterForm() { let mappedGender: UserGender | null = null; - if (formData.genero === 'hombre') { + if (formData.gender === 'hombre') { mappedGender = UserGender.MALE; - } else if (formData.genero === 'mujer') { + } else if (formData.gender === 'mujer') { mappedGender = UserGender.FEMALE; } const payload = { - firstName: formData.nombre, - lastName: formData.apellido, + firstName: formData.firstName, + lastName: formData.lastName, email: formData.email, password: formData.password, - documentId: formData.cedula, - birthDate: formData.fechaNacimiento, - phoneNumber: formData.telefono.trim() !== '' ? formData.telefono : null, + documentId: formData.documentId, + birthDate: formData.birthDate, + phoneNumber: + formData.phoneNumber.trim() !== '' ? formData.phoneNumber : null, gender: mappedGender, }; @@ -123,13 +124,13 @@ export default function RegisterForm() { console.log('SignUp response:', response); toast.success('Cuenta creada correctamente'); setFormData({ - nombre: '', - apellido: '', + firstName: '', + lastName: '', email: '', - cedula: '', - telefono: '', - fechaNacimiento: '', - genero: '', + documentId: '', + phoneNumber: '', + birthDate: '', + gender: '', password: '', confirmPassword: '', }); @@ -140,7 +141,6 @@ export default function RegisterForm() { }); login(loginResponse.accessToken, false); - window.location.reload(); router.push('/'); } catch (err) { console.error('Error creating account:', err); @@ -180,30 +180,30 @@ export default function RegisterForm() {

- {errors.nombre} + {errors.firstName}

- {errors.apellido} + {errors.lastName}

@@ -229,15 +229,15 @@ export default function RegisterForm() {

- {errors.cedula} + {errors.documentId}

@@ -246,16 +246,16 @@ export default function RegisterForm() {

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

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

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

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

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

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/Badge.tsx b/src/components/Badge.tsx index 86725c1..5cb0952 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 = { filled: colorClasses[color], outlined: outlinedClasses[color], text: textClasses[color], }; + return ( {children} ); }; + export default Badge; diff --git a/src/components/CardButton.tsx b/src/components/CardButton.tsx index a31c157..4d63b90 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}`; @@ -72,30 +78,31 @@ 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; 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/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 3e66406..c24c4a3 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -13,10 +13,12 @@ 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'; interface UserProfile { + id: string; firstName: string; lastName: string; profile: { @@ -30,64 +32,59 @@ type NavBarProps = { export default function NavBar({ onCartClick }: NavBarProps) { const router = useRouter(); - const [categories, setCategories] = useState([]); - const { cartItems } = useCart(); - const { token, user } = useAuth(); - const totalCount = cartItems.reduce((acc, item) => acc + item.quantity, 0); + const { itemsCount } = useCart(); + const { token, user, isLoading } = useAuth(); + const { + notifications, + notificationCount, + isNotificationsOpen, + toggleNotifications, + panelRef, + } = useNotifications(token ?? undefined); const [isLoggedIn, setIsLoggedIn] = useState(false); const [userData, setUserData] = useState(null); - const [isCartOpen, setIsCartOpen] = useState(false); + const [isCartOpen, setIsCartOpen] = useState(false); - useEffect(() => { - api.category - .findAll({ page: 1, limit: 20 }) - .then((resp: Pagination) => { - if (resp && resp.results) { - setCategories(resp.results); - } - }) - .catch((err: unknown) => { - console.error('Error al cargar categorías:', err); - }); - }, []); + const [showLogin, setShowLogin] = useState(false); - // Obtener perfil si está logueado useEffect(() => { - if (!token || !user?.sub) { + if (token && user?.sub) { + setIsLoggedIn(true); + (async () => { + try { + const profileResponse = await api.user.getProfile(user.sub, token); + setUserData(profileResponse); + } catch (error) { + console.error('Error al obtener perfil:', error); + setUserData(null); + } + })(); + } else { setIsLoggedIn(false); setUserData(null); - return; } - - setIsLoggedIn(true); - - (async () => { - 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); - } - })(); }, [token, user]); - const handleSearch = (query: string, category: string) => { - console.log('Buscando:', query, 'en', category); - }; + useEffect(() => { + const timeout = setTimeout(() => { + setShowLogin(true); + }, 1000); + return () => clearTimeout(timeout); + }, []); const handleLoginClick = () => { router.push('/login'); }; + if (isLoading) return null; + return ( <> - {/* Cart Overlay */} setIsCartOpen(false)} /> - {/* Versión Desktop */} -