diff --git a/src/app/(shop)/layout.tsx b/src/app/(shop)/layout.tsx index 5f23b1a..bb0ec5b 100644 --- a/src/app/(shop)/layout.tsx +++ b/src/app/(shop)/layout.tsx @@ -3,12 +3,17 @@ import { ReactNode } from 'react'; import NavBar from '@/components/Navbar'; import Footer from '@/components/Footer'; +import { useAuth } from '@/context/AuthContext'; type ShopLayoutProps = { children: ReactNode; }; export default function ShopLayout({ children }: ShopLayoutProps) { + const { isLoading } = useAuth(); + + if (isLoading) return null; + return (
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 */}