Skip to content

Commit 05b8c54

Browse files
authored
Merge pull request #415 from map-of-pi/bug/fix-notifications
Approved (1).
2 parents a999898 + 1a9b5f5 commit 05b8c54

File tree

10 files changed

+196
-94
lines changed

10 files changed

+196
-94
lines changed

context/AppContextProvider.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ interface IAppContextProps {
3030
isSaveLoading: boolean;
3131
setIsSaveLoading: React.Dispatch<SetStateAction<boolean>>;
3232
adsSupported: boolean;
33-
toggleNotification: boolean;
34-
setToggleNotification: React.Dispatch<SetStateAction<boolean>>;
3533
}
3634

3735
const initialState: IAppContextProps = {
@@ -47,9 +45,7 @@ const initialState: IAppContextProps = {
4745
setReload: () => {},
4846
isSaveLoading: false,
4947
setIsSaveLoading: () => {},
50-
adsSupported: false,
51-
toggleNotification: false,
52-
setToggleNotification: () => {},
48+
adsSupported: false
5349
};
5450

5551
export const AppContext = createContext<IAppContextProps>(initialState);
@@ -66,7 +62,6 @@ const AppContextProvider = ({ children }: AppContextProviderProps) => {
6662
const [isSaveLoading, setIsSaveLoading] = useState(false);
6763
const [alertMessage, setAlertMessage] = useState<string | null>(null);
6864
const [adsSupported, setAdsSupported] = useState(false);
69-
const [toggleNotification, setToggleNotification] = useState<boolean>(true);
7065

7166
const showAlert = (message: string) => {
7267
setAlertMessage(message);
@@ -180,9 +175,7 @@ const AppContextProvider = ({ children }: AppContextProviderProps) => {
180175
setAlertMessage,
181176
isSaveLoading,
182177
setIsSaveLoading,
183-
adsSupported,
184-
toggleNotification,
185-
setToggleNotification
178+
adsSupported
186179
}}
187180
>
188181
{children}

src/app/[locale]/notification/page.tsx

Lines changed: 42 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import React, { useContext, useEffect, useRef, useState } from 'react';
55
import NotificationCard from '@/components/shared/Notification/NotificationCard';
66
import Skeleton from '@/components/skeleton/skeleton';
77
import { NotificationType } from '@/constants/types';
8+
import { useScrollablePagination } from '@/hooks/useScrollablePagination';
89
import { getNotifications, updateNotification } from '@/services/notificationApi';
910
import { AppContext } from '../../../../context/AppContextProvider';
1011
import logger from '../../../../logger.config.mjs';
@@ -14,13 +15,21 @@ export default function NotificationPage() {
1415
const { currentUser } = useContext(AppContext);
1516

1617
const [notifications, setNotifications] = useState<NotificationType[]>([]);
18+
const loadMoreRef = useRef<HTMLDivElement | null>(null);
1719
const [skip, setSkip] = useState(0);
18-
const [limit] = useState(10);
20+
const [limit] = useState(5);
1921
const [isLoading, setLoading] = useState(false);
22+
const [hasFetched, setHasFetched] = useState(false);
23+
const [hasMore, setHasMore] = useState(true);
2024

21-
const container = useRef<HTMLDivElement[]>([]);
25+
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
2226
const observer = useRef<IntersectionObserver | null>(null);
23-
const loadMoreObserver = useRef<IntersectionObserver | null>(null);
27+
28+
const handleShopItemRef = (node: HTMLElement | null) => {
29+
if (node && observer.current) {
30+
observer.current.observe(node);
31+
}
32+
};
2433

2534
const handleUpdateNotification = async (id: string) => {
2635
const prev = notifications.find((n) => n._id === id);
@@ -56,8 +65,7 @@ export default function NotificationPage() {
5665
};
5766

5867
const fetchNotifications = async () => {
59-
if (isLoading || !currentUser?.pi_uid) return;
60-
setLoading(true);
68+
if (isLoading || !currentUser?.pi_uid || !hasMore) return;
6169

6270
try {
6371
const newNotifications = await getNotifications({
@@ -71,60 +79,39 @@ export default function NotificationPage() {
7179
if (newNotifications.length > 0) {
7280
const sorted = sortNotifications(notifications, newNotifications);
7381
setNotifications(sorted);
74-
setSkip((prev) => prev + limit);
82+
setSkip(skip + limit);
83+
}
84+
85+
if (newNotifications.length < limit) {
86+
setHasMore(false); // No more pages
7587
}
7688
} catch (error) {
7789
logger.error('Error fetching notifications:', error);
7890
} finally {
91+
setHasFetched(true);
7992
setLoading(false);
8093
}
8194
};
8295

8396
useEffect(() => {
8497
if (!currentUser?.pi_uid) return;
98+
8599
setNotifications([]);
86100
setSkip(0);
87101
fetchNotifications();
88102
}, [currentUser?.pi_uid]);
89103

90-
useEffect(() => {
91-
if (observer.current) observer.current.disconnect();
92-
if (loadMoreObserver.current) loadMoreObserver.current.disconnect();
93-
94-
observer.current = new IntersectionObserver(
95-
(entries) => {
96-
entries.forEach((entry) => {
97-
entry.target.classList.toggle('show', entry.isIntersecting);
98-
if (entry.isIntersecting) observer.current?.unobserve(entry.target);
99-
});
100-
},
101-
{ threshold: 0.5 }
102-
);
103-
104-
container.current.forEach((el) => {
105-
if (el) observer.current?.observe(el);
106-
});
107-
108-
loadMoreObserver.current = new IntersectionObserver(
109-
(entries) => {
110-
const lastEntry = entries[0];
111-
if (lastEntry?.isIntersecting) {
112-
fetchNotifications();
113-
loadMoreObserver.current?.unobserve(lastEntry.target);
114-
}
115-
},
116-
{ threshold: 1 }
117-
);
118-
119-
const lastItem = container.current[container.current.length - 1];
120-
if (lastItem) loadMoreObserver.current.observe(lastItem);
121-
122-
return () => {
123-
observer.current?.disconnect();
124-
loadMoreObserver.current?.disconnect();
125-
};
126-
}, [notifications]);
127-
104+
useScrollablePagination({
105+
containerRef: scrollContainerRef,
106+
loadMoreRef,
107+
fetchNextPage: async () => {
108+
setLoading(true);
109+
await fetchNotifications();
110+
},
111+
hasMore,
112+
isLoading,
113+
});
114+
128115
return (
129116
<div className="w-full md:w-[500px] md:mx-auto p-4">
130117
<div className="text-center mb-7">
@@ -134,8 +121,12 @@ export default function NotificationPage() {
134121
</div>
135122

136123
{/* Notifications */}
137-
<div>
138-
{!isLoading && notifications.length === 0 ? (
124+
<div
125+
ref={scrollContainerRef}
126+
id="notification-scroll-container"
127+
className="max-h-[600px] overflow-y-auto p-1 mb-7 mt-3"
128+
>
129+
{!isLoading && hasFetched && notifications.length === 0 ? (
139130
<h2 className="font-bold mb-2 text-center">
140131
{t('SCREEN.NOTIFICATIONS.NO_NOTIFICATIONS_SUBHEADER')}
141132
</h2>
@@ -145,15 +136,15 @@ export default function NotificationPage() {
145136
key={notify._id}
146137
notification={notify}
147138
onToggleClear={handleUpdateNotification}
148-
forwardedRef={(el) => {
149-
if (el) container.current[index] = el;
150-
}}
139+
refCallback={handleShopItemRef} // Attach observer
151140
/>
152141
))
153142
)}
143+
144+
{/* Load more trigger */}
145+
{isLoading && <Skeleton type="notification" />}
146+
<div ref={loadMoreRef} className="h-[20px]" />
154147
</div>
155-
156-
{isLoading && <Skeleton type="notification" />}
157148
</div>
158149
);
159150
}

src/app/[locale]/page.tsx

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Button } from '@/components/shared/Forms/Buttons/Buttons';
1111
import SearchBar from '@/components/shared/SearchBar/SearchBar';
1212
import ConfirmDialog from '@/components/shared/confirm';
1313
import NotificationDialog from '@/components/shared/Notification/NotificationDialog';
14+
import { getNotifications } from '@/services/notificationApi';
1415
import { fetchSellers } from '@/services/sellerApi';
1516
import { fetchUserSettings } from '@/services/userSettingsApi';
1617
import { DeviceLocationType, IUserSettings } from '@/constants/types';
@@ -29,26 +30,21 @@ export default function Page({ params }: { params: { locale: string } }) {
2930
const mapRef = useRef<L.Map | null>(null);
3031

3132
// State management with proper typing
32-
const [mapCenter, setMapCenter] = useState<{ lat: number; lng: number } | null>(null);
3333
const [searchCenter, setSearchCenter] = useState<{ lat: number; lng: number } | null>(null);
34-
const [findme, setFindme] = useState<DeviceLocationType>(DeviceLocationType.SearchCenter);
3534
const [dbUserSettings, setDbUserSettings] = useState<IUserSettings | null>(null);
3635
const [zoomLevel, setZoomLevel] = useState(2);
37-
const [locationError, setLocationError] = useState<string | null>(null);
38-
const [searchBarValue, setSearchBarValue] = useState('');
3936
const [searchQuery, setSearchQuery] = useState<string>('');
4037
const [isSearchClicked, setSearchClicked] = useState(false);
4138
const [searchResults, setSearchResults] = useState<any[]>([]);
42-
const [showPopup, setShowPopup] = useState<boolean>(false);
39+
const [showSearchCenterPopup, setSearchCenterPopup] = useState<boolean>(false);
40+
const [showNotificationPopup, setShowNotificationPopup] = useState<boolean>(false);
4341

4442
const {
4543
isSigningInUser,
4644
currentUser,
4745
autoLoginUser,
4846
reload,
49-
setReload,
50-
toggleNotification,
51-
setToggleNotification
47+
setReload
5248
} = useContext(AppContext);
5349

5450
useEffect(() => {
@@ -58,7 +54,7 @@ export default function Page({ params }: { params: { locale: string } }) {
5854
sessionStorage.removeItem('prevMapZoom');
5955
}
6056
setReload(false);
61-
setShowPopup(false);
57+
setSearchCenterPopup(false);
6258
checkAndAutoLoginUser(currentUser, autoLoginUser);
6359

6460
const getUserSettingsData = async () => {
@@ -67,14 +63,16 @@ export default function Page({ params }: { params: { locale: string } }) {
6763
if (data) {
6864
logger.info('Fetched user settings data successfully:', { data });
6965
setDbUserSettings(data);
70-
if (data.search_map_center?.coordinates) {
71-
const coordinates = {
72-
lat: data.search_map_center.coordinates[1],
73-
lng: data.search_map_center.coordinates[0],
66+
67+
const coordinates = data.search_map_center?.coordinates;
68+
if (coordinates) {
69+
const searchMapCenter = {
70+
lat: coordinates[1],
71+
lng: coordinates[0],
7472
};
75-
setSearchCenter(coordinates);
76-
if (coordinates.lat === 0 && coordinates.lng === 0) {
77-
setShowPopup(true);
73+
setSearchCenter(searchMapCenter);
74+
if (searchMapCenter.lat === 0 && searchMapCenter.lng === 0) {
75+
setSearchCenterPopup(true);
7876
}
7977
}
8078
} else {
@@ -87,7 +85,37 @@ export default function Page({ params }: { params: { locale: string } }) {
8785
}
8886
};
8987

88+
const checkUnclearedNotifications = async () => {
89+
if (!currentUser?.pi_uid) return;
90+
91+
const hasShownNotificationThisSession = sessionStorage.getItem('notificationShown');
92+
if (hasShownNotificationThisSession === 'true') return; // Don't show again this session
93+
94+
try {
95+
const notifications = await getNotifications({
96+
pi_uid: currentUser.pi_uid,
97+
skip: 0,
98+
limit: 0,
99+
status: 'uncleared'
100+
});
101+
102+
console.log('Uncleared notifications response:', notifications);
103+
104+
if (notifications?.length > 0) {
105+
setShowNotificationPopup(true);
106+
sessionStorage.setItem('notificationShown', 'true');
107+
} else {
108+
setShowNotificationPopup(false);
109+
}
110+
} catch (error) {
111+
logger.error('Error getting new notifications:', error);
112+
setShowNotificationPopup(false);
113+
}
114+
};
115+
116+
// Only proceed once currentUser is defined with a valid pi_uid
90117
getUserSettingsData();
118+
checkUnclearedNotifications();
91119
}, [currentUser, reload]);
92120

93121
useEffect(() => {
@@ -112,7 +140,7 @@ export default function Page({ params }: { params: { locale: string } }) {
112140
const loc = await userLocation(dbUserSettings);
113141
if (loc) {
114142
setSearchCenter({ lat: loc[0], lng: loc[1] });
115-
logger.info('User location obtained successfully on button click:', {location});
143+
logger.info('User location obtained successfully on button click:', {loc});
116144
} else {
117145
setSearchCenter(null);
118146
}
@@ -198,21 +226,22 @@ export default function Page({ params }: { params: { locale: string } }) {
198226
/>
199227
</div>
200228
</div>
201-
{showPopup && (
229+
{showSearchCenterPopup && (
202230
<ConfirmDialog
203-
show={setShowPopup}
204-
onClose={() => setShowPopup(false)}
231+
show={setSearchCenterPopup}
232+
onClose={() => setSearchCenterPopup(false)}
205233
message={t('HOME.SEARCH_CENTER_DEFAULT_MESSAGE')}
206234
url={`/map-center?entryType=search`}
207235
/>
208236
)}
209237
</div>
210-
{toggleNotification && (
238+
239+
{showNotificationPopup && (
211240
<NotificationDialog
241+
setShowDialog={setShowNotificationPopup}
242+
onClose={() => setShowNotificationPopup(false)}
212243
message={t('HOME.NEW_NOTIFICATIONS_MESSAGE')}
213-
onClose={() => setShowPopup(false)}
214244
url={`/${locale}/notification`}
215-
setToggleNotification={setToggleNotification}
216245
/>
217246
)}
218247
</>

src/components/shared/Notification/NotificationCard.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import { NotificationType } from '@/constants/types';
99
type NotificationCardProps = {
1010
notification: NotificationType;
1111
onToggleClear: (id: string) => void;
12-
forwardedRef?: React.Ref<HTMLDivElement>; // optional for scroll observer
12+
refCallback: (node: HTMLElement | null) => void;
1313
};
1414

1515
export default function NotificationCard({
1616
notification,
1717
onToggleClear,
18-
forwardedRef,
18+
refCallback,
1919
}: NotificationCardProps) {
2020
const t = useTranslations();
2121
const locale = useLocale();
@@ -31,7 +31,8 @@ export default function NotificationCard({
3131

3232
return (
3333
<div
34-
ref={forwardedRef}
34+
ref={refCallback}
35+
data-id={notification._id}
3536
className={`relative outline outline-50 outline-gray-600 rounded-lg mb-7
3637
transition-all duration-150 ease-in-out transform
3738
${notification.is_cleared ? 'bg-yellow-100' : ''}

src/components/shared/Notification/NotificationDialog.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ import { useRouter } from 'next/navigation';
33
import React from 'react';
44
import { IoMdClose } from 'react-icons/io';
55

6-
function NotificationDialog({ onClose, message, url, setToggleNotification } : any) {
6+
function NotificationDialog({ setShowDialog, onClose, message, url } : any) {
77
const t = useTranslations();
88
const router = useRouter();
99

1010
const handleClicked = () => {
1111
router.push(url);
12-
setToggleNotification(false);
12+
setShowDialog(false);
1313
}
1414

1515
const handleClose = () => {
16-
onClose;
17-
setToggleNotification(false);
16+
onClose?.();
17+
setShowDialog(false);
1818
}
1919

2020
return (

0 commit comments

Comments
 (0)