From 5a30acc6b702045e9376f4dbe5ac612ef776915a Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Sun, 18 May 2025 08:13:45 -0400 Subject: [PATCH 1/6] fix in style badge color, mobile and catching event for counter --- src/components/Navbar.tsx | 85 ++++++++++++++---------- src/components/User/NotificationBell.tsx | 43 ++++++++++++ 2 files changed, 94 insertions(+), 34 deletions(-) create mode 100644 src/components/User/NotificationBell.tsx diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 5ad6cc4..a894071 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'; @@ -20,7 +16,7 @@ import { useAuth } from '@/context/AuthContext'; import { api, API_URL } from '@/lib/sdkConfig'; import { CategoryResponse, Pagination } from '@pharmatech/sdk'; import CartOverlay from './Cart/CartOverlay'; -import NotificationList from '@/components/User/NotificationList'; +import NotificationBell from '@/components/User/NotificationBell'; interface UserProfile { firstName: string; @@ -112,8 +108,20 @@ export default function NavBar({ onCartClick }: NavBarProps) { } }, onmessage(event) { - console.log('New message from server', event); - setNotificationCount((prev) => prev + 1); + try { + if (!event.data) return; + + const parsed = JSON.parse(event.data); + + if (parsed?.message && parsed?.order?.id) { + console.log('New message from server', event); + setNotificationCount((prev) => prev + 1); + } else { + console.log('Ignored message from server', parsed); + } + } catch (err) { + console.log('Error parsing message from server', err); + } }, onclose() { console.log('Connection closed by the server'); @@ -182,25 +190,16 @@ export default function NavBar({ onCartClick }: NavBarProps) { {/* Notificaciones */} -
-
{ - setNotificationCount(0); - setIsNotificationsOpen((prev) => !prev); - }} - > -
- {notificationCount} -
- -
- {isNotificationsOpen && ( -
- -
- )} -
+ { + setNotificationCount(0); + setIsNotificationsOpen((prev) => !prev); + }} + refProp={notificationsRef} + /> {/* Usuario */} {isLoggedIn && userData ? ( @@ -228,7 +227,8 @@ export default function NavBar({ onCartClick }: NavBarProps) { {/* Versión Mobile */} @@ -237,12 +232,12 @@ export default function NavBar({ onCartClick }: NavBarProps) { withDropdown={true} onProfileClick={() => router.push('/user')} /> - ) : ( + ) : showLogin ? ( - )} + ) : null} {/* Columna centro: logo */} diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 8aa8ac7..64650f5 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -23,6 +23,7 @@ interface AuthContextType { user: JwtPayload | null; login: (token: string, remember: boolean) => void; logout: () => void; + isLoading: boolean; } const AuthContext = createContext({ @@ -30,6 +31,7 @@ const AuthContext = createContext({ user: null, login: () => {}, logout: () => {}, + isLoading: true, }); const decodeToken = (rawToken: string): JwtPayload | null => { @@ -52,6 +54,8 @@ const decodeToken = (rawToken: string): JwtPayload | null => { export const AuthProvider = ({ children }: { children: ReactNode }) => { const [token, setToken] = useState(null); const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const router = useRouter(); useEffect(() => { @@ -64,6 +68,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const decoded = decodeToken(storedToken); setUser(decoded); } + setIsLoading(false); }, []); useEffect(() => { @@ -104,7 +109,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { }; return ( - + {children} ); diff --git a/src/lib/utils/constants/DateUtils.ts b/src/lib/utils/constants/DateUtils.ts index 1353abc..16818b0 100644 --- a/src/lib/utils/constants/DateUtils.ts +++ b/src/lib/utils/constants/DateUtils.ts @@ -18,6 +18,7 @@ export const WEEK_DAYS = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb']; export const formatTimeAgo = (date: string): string => { const created = new Date(date); const diff = Math.floor((Date.now() - created.getTime()) / 1000); + if (diff < 0) return 'Hace unos segundos'; if (diff < 60) return `Hace ${diff} segundos`; if (diff < 3600) return `Hace ${Math.floor(diff / 60)} minutos`; if (diff < 86400) return `Hace ${Math.floor(diff / 3600)} horas`; From 9d94ad94b4d54904c82475ba96e2ad32e15172a4 Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Sun, 18 May 2025 10:04:41 -0400 Subject: [PATCH 3/6] fix in navigation state log out in navbar --- .../presentation/[presentationId]/page.tsx | 3 +++ src/context/AuthContext.tsx | 13 +++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx index 18fd5cc..2295ce8 100644 --- a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx +++ b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx @@ -20,11 +20,13 @@ import { } from '@pharmatech/sdk'; import Loading from '@/app/loading'; import ProductNotFound from '@/components/Product/NotFound'; +import { useAuth } from '@/context/AuthContext'; export default function ProductDetailPage() { const router = useRouter(); const params = useParams(); const searchParams = useSearchParams(); + const { isLoading } = useAuth(); // Detect if we came from a filtered search const queryString = searchParams?.toString() || ''; @@ -131,6 +133,7 @@ export default function ProductDetailPage() { if (found) router.push(`/product/${productId}/presentation/${found.id}`); }; + if (isLoading) return ; return (
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 64650f5..e791c1c 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -37,13 +37,10 @@ const AuthContext = createContext({ const decodeToken = (rawToken: string): JwtPayload | null => { try { const decoded = jwtDecode(rawToken); - - // (Opcional) Verificar expiración if (decoded.exp && decoded.exp * 1000 < Date.now()) { console.warn('Token expirado'); return null; } - return decoded; } catch (error) { console.error('Error decoding token:', error); @@ -60,8 +57,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { useEffect(() => { const storedToken = - sessionStorage.getItem('pharmatechToken') || - localStorage.getItem('pharmatechToken'); + localStorage.getItem('pharmatechToken') || + sessionStorage.getItem('pharmatechToken'); if (storedToken) { setToken(storedToken); @@ -89,9 +86,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { }, []); const login = (newToken: string, remember: boolean) => { - sessionStorage.setItem('pharmatechToken', newToken); - if (remember) { - localStorage.setItem('pharmatechToken', newToken); + localStorage.setItem('pharmatechToken', newToken); + if (!remember) { + sessionStorage.setItem('pharmatechToken', newToken); } setToken(newToken); From 52b391410e3397933a1592c5f878362881777496 Mon Sep 17 00:00:00 2001 From: ClarissaRun Date: Sun, 18 May 2025 13:54:01 -0400 Subject: [PATCH 4/6] changes requested in loading state and time in notification --- .../presentation/[presentationId]/page.tsx | 9 +- src/components/Navbar.tsx | 110 +++++++++++------- src/components/User/NotificationBell.tsx | 5 +- src/components/User/NotificationList.tsx | 42 ++----- src/lib/utils/constants/DateUtils.ts | 23 +++- 5 files changed, 101 insertions(+), 88 deletions(-) diff --git a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx index 2295ce8..766afdf 100644 --- a/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx +++ b/src/app/(shop)/product/[productId]/presentation/[presentationId]/page.tsx @@ -18,15 +18,12 @@ import { ProductPresentation, ProductPaginationRequest, } from '@pharmatech/sdk'; -import Loading from '@/app/loading'; import ProductNotFound from '@/components/Product/NotFound'; -import { useAuth } from '@/context/AuthContext'; export default function ProductDetailPage() { const router = useRouter(); const params = useParams(); const searchParams = useSearchParams(); - const { isLoading } = useAuth(); // Detect if we came from a filtered search const queryString = searchParams?.toString() || ''; @@ -44,7 +41,6 @@ export default function ProductDetailPage() { const [presentationList, setPresentationList] = useState< ProductPresentationResponse[] >([]); - const [loading, setLoading] = useState(true); // 1) Load presentation detail useEffect(() => { @@ -104,11 +100,9 @@ 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 ; // Breadcrumb con acción de "volver" si es búsqueda personalizada @@ -133,7 +127,6 @@ export default function ProductDetailPage() { if (found) router.push(`/product/${productId}/presentation/${found.id}`); }; - if (isLoading) return ; return (
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index a43b6ea..314cd5e 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -14,7 +14,11 @@ 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 NotificationBell from '@/components/User/NotificationBell'; @@ -41,9 +45,12 @@ export default function NavBar({ onCartClick }: NavBarProps) { 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 @@ -51,7 +58,7 @@ 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); }); }, []); @@ -66,7 +73,6 @@ export default function NavBar({ onCartClick }: NavBarProps) { } catch (error) { console.error('Error al obtener perfil:', error); setUserData(null); - } finally { } })(); } else { @@ -79,61 +85,63 @@ export default function NavBar({ onCartClick }: NavBarProps) { const timeout = setTimeout(() => { setShowLogin(true); }, 1000); - return () => clearTimeout(timeout); }, []); - // 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); - }, []); - + // Fetch notifications stream useEffect(() => { const controller = new AbortController(); + const fetchData = async () => { await fetchEventSource(`${API_URL}/notification/stream`, { headers: { Authorization: `Bearer ${token}`, }, + signal: controller.signal, async onopen(res) { if (res.ok && res.status === 200) { - console.log('Connection made ', res); + console.log('Connection made', res); } else if ( res.status >= 400 && res.status < 500 && res.status !== 429 ) { - console.log('Client side error ', res); + console.log('Client side error', res); } }, onmessage(event) { console.log('New message from server', event); setNotificationCount((prev) => prev + 1); }, - onerror(err) { console.log('There was an error from server', err); }, }); }; + if (token) { fetchData(); } + return () => { controller.abort(); console.log('Connection aborted'); }; }, [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); }; @@ -141,14 +149,48 @@ export default function NavBar({ onCartClick }: NavBarProps) { const handleLoginClick = () => { 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 */}