diff --git a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx
index 18fd5cc..8814aa9 100644
--- a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx
+++ b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx
@@ -50,7 +50,8 @@ export default function ProductDetailPage() {
api.productPresentation
.getByPresentationId(productId, presentationId)
.then((data) => setPresentation(data))
- .catch((err) => console.error(err));
+ .catch((err) => console.error(err))
+ .finally(() => setLoading(false));
}, [productId, presentationId]);
// 2) Load generic product info & variants
@@ -102,10 +103,8 @@ export default function ProductDetailPage() {
api.product
.getProducts(req)
.then((res) => setProducts(res.results))
- .catch((err) => console.error(err))
- .finally(() => setLoading(false));
+ .catch((err) => console.error(err));
}, [genericProduct]);
-
if (loading) return
;
if (!presentation || !genericProduct) return
;
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index aa67b03..306c66e 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -3,11 +3,7 @@
import React, { useEffect, useRef, useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';
-import {
- ShoppingCartIcon,
- UserCircleIcon,
- BellIcon,
-} from '@heroicons/react/24/outline';
+import { ShoppingCartIcon, UserCircleIcon } from '@heroicons/react/24/outline';
import { fetchEventSource } from '@microsoft/fetch-event-source';
import Avatar from '@/components/Avatar';
import SearchBar from '@/components/SearchBar';
@@ -18,9 +14,13 @@ import Button from '@/components/Button';
import { useCart } from '@/context/CartContext';
import { useAuth } from '@/context/AuthContext';
import { api, API_URL } from '@/lib/sdkConfig';
-import { CategoryResponse, Pagination } from '@pharmatech/sdk';
+import {
+ CategoryResponse,
+ Pagination,
+ NotificationResponse,
+} from '@pharmatech/sdk';
import CartOverlay from './Cart/CartOverlay';
-import NotificationList from '@/components/User/NotificationList';
+import NotificationBell from '@/components/User/NotificationBell';
interface UserProfile {
id: string;
@@ -41,13 +41,17 @@ export default function NavBar({ onCartClick }: NavBarProps) {
const [isNotificationsOpen, setIsNotificationsOpen] = useState(false);
const notificationsRef = useRef
(null);
const { cartItems } = useCart();
- const { token, user } = useAuth();
+ const { token, user, isLoading } = useAuth();
const totalCount = cartItems.reduce((acc, item) => acc + item.quantity, 0);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [userData, setUserData] = useState(null);
- const [isCartOpen, setIsCartOpen] = useState(false);
+ const [isCartOpen, setIsCartOpen] = useState(false);
const [notificationCount, setNotificationCount] = useState(0);
+ const [showLogin, setShowLogin] = useState(false);
+ const [notifications, setNotifications] = useState(
+ [],
+ );
useEffect(() => {
api.category
@@ -55,85 +59,96 @@ export default function NavBar({ onCartClick }: NavBarProps) {
.then((resp: Pagination) => {
if (resp?.results) setCategories(resp.results);
})
- .catch((err: unknown) => {
+ .catch((err) => {
console.error('Error al cargar categorías:', err);
});
}, []);
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);
- } catch (error) {
- console.error('Error al obtener perfil:', error);
- setUserData(null);
- }
- })();
}, [token, user]);
- // Cerrar dropdown si se hace click fuera
useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (
- notificationsRef.current &&
- !notificationsRef.current.contains(event.target as Node)
- ) {
- setIsNotificationsOpen(false);
- }
- };
- document.addEventListener('mousedown', handleClickOutside);
- return () => document.removeEventListener('mousedown', handleClickOutside);
+ const timeout = setTimeout(() => {
+ setShowLogin(true);
+ }, 1000);
+ return () => clearTimeout(timeout);
}, []);
+ // Fetch notifications stream
useEffect(() => {
- const controller = new AbortController();
- const fetchData = async () => {
- await fetchEventSource(`${API_URL}/notification/stream`, {
- headers: {
- Authorization: `Bearer ${token}`,
- },
+ if (!token) return;
+
+ let aborted = false;
+ let retryId: NodeJS.Timeout;
+
+ const connect = () => {
+ fetchEventSource(`${API_URL}/notification/stream`, {
+ headers: { Authorization: `Bearer ${token}` },
+ openWhenHidden: true,
async onopen(res) {
- if (res.ok && res.status === 200) {
- console.log('Connection made ', res);
- } else if (
- res.status >= 400 &&
- res.status < 500 &&
- res.status !== 429
- ) {
- console.log('Client side error ', res);
- }
+ if (res.ok) console.log('SSE abierta');
},
- onmessage(event) {
- if (event.event == 'notification') {
- setNotificationCount((prev) => prev + 1);
- }
+ onmessage(ev) {
+ if (!ev.data) return;
+
+ try {
+ const d = JSON.parse(ev.data);
+ if (d.type !== 'notification') return;
+
+ setNotifications((prev) => {
+ const exists = prev.some((n) => n.id === d.payload.id);
+ if (exists) return prev;
+
+ setNotificationCount((c) => c + 1);
+ return [d.payload, ...prev];
+ });
+ } catch {}
},
onclose() {
- console.log('Connection closed by the server');
+ if (!aborted) retryId = setTimeout(connect, 5000);
},
- onerror(err) {
- console.log('There was an error from server', err);
+ onerror() {
+ if (!aborted) retryId = setTimeout(connect, 5000);
},
});
};
- if (token) {
- fetchData();
- }
+
+ connect();
+
return () => {
- controller.abort();
- console.log('Connection aborted');
+ aborted = true;
+ clearTimeout(retryId);
};
}, [token]);
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ notificationsRef.current &&
+ !notificationsRef.current.contains(event.target as Node)
+ ) {
+ setIsNotificationsOpen(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
const handleSearch = (query: string, category: string) => {
console.log('Buscando:', query, 'en', category);
};
@@ -142,12 +157,47 @@ export default function NavBar({ onCartClick }: NavBarProps) {
router.push('/login');
};
+ const handleNotificationToggle = async () => {
+ const willOpen = !isNotificationsOpen;
+
+ if (willOpen && token) {
+ try {
+ const res = await api.notification.getNotifications(token);
+ if (Array.isArray(res)) {
+ setNotifications(res);
+
+ const unread = res.filter((n) => !n.isRead);
+ setNotificationCount(unread.length);
+ // Mark notifications as read
+ if (unread.length > 0) {
+ await Promise.all(
+ unread.map((notif) =>
+ api.notification.markAsRead(notif.order.id, token),
+ ),
+ );
+ setNotifications((prev) =>
+ prev.map((n) =>
+ unread.some((u) => u.id === n.id) ? { ...n, isRead: true } : n,
+ ),
+ );
+ setNotificationCount(0);
+ }
+ }
+ } catch (err) {
+ console.error('Error fetching notifications:', err);
+ }
+ }
+
+ setIsNotificationsOpen(willOpen);
+ };
+
+ if (isLoading) return null;
+
return (
<>
- {/* Cart Overlay */}
setIsCartOpen(false)} />
- {/* Versión Desktop */}
+ {/* Desktop Nav */}
- {/* Versión Mobile */}
+ {/* Mobile Nav */}