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}
@@ -229,15 +229,15 @@ export default function RegisterForm() {
- {errors.cedula}
+ {errors.documentId}
@@ -246,16 +246,16 @@ export default function RegisterForm() {
@@ -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)}