Skip to content

Commit b415b68

Browse files
committed
feat: Implement infinite scroll for hackathons page and enhance loading state management
- Added hook to manage loading more hackathons as the user scrolls. - Updated to utilize the new hook and log sentinel element references. - Refactored loading state display to improve user experience. - Adjusted to support pagination and logging for better debugging. - Modified API response handling to include pagination details for hackathons.
1 parent 64fcdda commit b415b68

8 files changed

Lines changed: 66 additions & 91 deletions

File tree

components/hackathons/HackathonsPage.tsx

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ interface HackathonsPageProps {
1717
className?: string;
1818
}
1919

20-
export default function HackathonsPage({
21-
className,
22-
}: HackathonsPageProps = {}) {
20+
const HackathonsPage: React.FC<HackathonsPageProps> = ({ className } = {}) => {
2321
const {
2422
filters,
2523
handleSearch,
@@ -56,13 +54,6 @@ export default function HackathonsPage({
5654

5755
const [loadMoreSentinelEl, setLoadMoreSentinelRef] =
5856
useState<HTMLDivElement | null>(null);
59-
const setLoadMoreSentinelRefWithLog = React.useCallback(
60-
(el: HTMLDivElement | null) => {
61-
console.log('[HackathonsPage] Sentinel ref set', el ? 'element' : 'null');
62-
setLoadMoreSentinelRef(el);
63-
},
64-
[]
65-
);
6657
useInfiniteScroll(loadMoreSentinelEl, {
6758
onLoadMore: loadMore,
6859
hasMore,
@@ -179,7 +170,7 @@ export default function HackathonsPage({
179170
</div>
180171

181172
<div
182-
ref={setLoadMoreSentinelRefWithLog}
173+
ref={setLoadMoreSentinelRef}
183174
className='h-px w-full'
184175
aria-hidden
185176
/>
@@ -195,4 +186,6 @@ export default function HackathonsPage({
195186
</main>
196187
</div>
197188
);
198-
}
189+
};
190+
191+
export default HackathonsPage;

components/hackathons/hackathonStickyCard.tsx

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -97,25 +97,14 @@ export function HackathonStickyCard(props: HackathonStickyCardProps) {
9797
case 'BEFORE_SUBMISSION_DEADLINE':
9898
return beforeStart ? 'Register' : 'Join';
9999
case 'CUSTOM':
100-
if (registrationDeadline) {
101-
const custom = new Date(registrationDeadline);
102-
const beforeCustom = now < custom;
103-
if (beforeStart && beforeCustom) return 'Register';
104-
if (!beforeStart && beforeCustom) return 'Register';
105-
}
106100
return 'Register';
107101
default:
108102
return 'Join';
109103
}
110-
}, [
111-
registrationDeadlinePolicy,
112-
canRegister,
113-
startDate,
114-
registrationDeadline,
115-
]);
104+
}, [registrationDeadlinePolicy, canRegister, startDate]);
116105

117106
// Redirect to auth if user not authenticated
118-
const redirectToAuth = () => {
107+
const handleRedirectToAuth = () => {
119108
const callbackUrl = encodeURIComponent(pathname);
120109
router.push(`/auth?mode=signin&callbackUrl=${callbackUrl}`);
121110
};
@@ -136,8 +125,7 @@ export function HackathonStickyCard(props: HackathonStickyCardProps) {
136125

137126
return (
138127
<div className='sticky top-18 hidden lg:block'>
139-
<Card className='overflow-hidden border border-[#a7f950]/30 bg-gradient-to-br from-[#a7f950]/10 to-transparent p-0'>
140-
{/* Image Section - UNCHANGED */}
128+
<Card className='overflow-hidden border border-[#a7f950]/30 bg-linear-to-br from-[#a7f950]/10 to-transparent p-0'>
141129
{imageUrl && (
142130
<div className='relative h-60 w-full'>
143131
<Image
@@ -192,17 +180,7 @@ export function HackathonStickyCard(props: HackathonStickyCardProps) {
192180
</div>
193181
)}
194182

195-
{/* Countdown Timer */}
196-
{/* {timeRemaining.total > 0 && (
197-
<div className="text-center py-2 rounded-lg bg-gray-900/50 border border-gray-800">
198-
<div className="text-xs text-gray-400 mb-0.5 uppercase tracking-wide">
199-
{status === "ongoing" ? "Ends In" : "Starts In"}
200-
</div>
201-
<div className="text-base font-bold text-white">
202-
{formatCountdown(timeRemaining)}
203-
</div>
204-
</div>
205-
)} */}
183+
{/* TODO: Add countdown UI — implement timeRemaining calculation and formatCountdown helper, then render "Ends In" / "Starts In" with formatted time */}
206184

207185
{/* Date Info Section */}
208186
<div className='space-y-1.5 text-sm'>
@@ -248,7 +226,9 @@ export function HackathonStickyCard(props: HackathonStickyCardProps) {
248226
) : !isRegistered ? (
249227
canRegister ? (
250228
<Button
251-
onClick={!isAuthenticated ? redirectToAuth : onJoinClick}
229+
onClick={
230+
!isAuthenticated ? handleRedirectToAuth : onJoinClick
231+
}
252232
className='w-full bg-[#a7f950] py-4 text-sm font-bold text-black hover:bg-[#8fd93f]'
253233
>
254234
<Calendar className='mr-1.5 h-3.5 w-3.5' />

content/blog/mdx-playground.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
---
22
title: 'MDX Playground — Test All Blog Features'
33
excerpt: 'A single post to verify frontmatter, prose styling, code blocks, tables, lists, blockquotes, and links. Use this to confirm the full MDX blog pipeline works end-to-end.'
4-
coverImage: 'https://images.unsplash.com/photo-1517694712202-14dd9538aa97?w=800&auto=format&fit=crop'
4+
coverImage: '/blog/mdx-playground-cover.jpg'
55
publishedAt: '2025-02-26'
66
author:
77
name: 'Boundless Dev'
8-
image: 'https://i.pravatar.cc/150?img=33'
8+
image: '/blog/mdx-playground-author.jpg'
99
categories: ['Platform', 'Technology', 'Education']
1010
tags: ['mdx', 'testing', 'playground', 'prose', 'markdown']
1111
readingTime: 8

hooks/hackathon/use-hackathons-list.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ interface UseHackathonsListReturn {
3636
refetch: () => void;
3737
}
3838

39-
export function useHackathonsList(
40-
options: UseHackathonsListOptions = {}
41-
): UseHackathonsListReturn {
39+
export const useHackathonsList: (
40+
options: UseHackathonsListOptions
41+
) => UseHackathonsListReturn = (options: UseHackathonsListOptions = {}) => {
4242
const { initialPage = 1, pageSize = 10, initialFilters = {} } = options;
4343

4444
const [hackathons, setHackathons] = React.useState<Hackathon[]>([]);
@@ -156,10 +156,6 @@ export function useHackathonsList(
156156
const fetchHackathons = React.useCallback(
157157
async (page: number, currentFilters: HackathonFilters, append = false) => {
158158
try {
159-
console.log('[useHackathonsList] fetchHackathons called', {
160-
page,
161-
append,
162-
});
163159
if (append) {
164160
setLoadingMore(true);
165161
} else {
@@ -186,13 +182,6 @@ export function useHackathonsList(
186182
await getPublicHackathonsList(apiFilters);
187183
let hackathonsList = response.hackathons || [];
188184

189-
console.log('[useHackathonsList] API response', {
190-
page,
191-
count: hackathonsList.length,
192-
total: response.total,
193-
hasMore: response.hasMore ?? false,
194-
});
195-
196185
// Apply client-side location filtering (API doesn't support it)
197186
hackathonsList = filterByLocation(
198187
hackathonsList,
@@ -243,20 +232,10 @@ export function useHackathonsList(
243232
}, [filters, fetchHackathons]);
244233

245234
const loadMore = React.useCallback(() => {
246-
console.log('[useHackathonsList] loadMore called', {
247-
loadingMore,
248-
hasMore,
249-
currentPage,
250-
});
251235
if (!loadingMore && hasMore) {
252236
const nextPage = currentPage + 1;
253-
console.log('[useHackathonsList] Fetching next page', nextPage);
254237
setCurrentPage(nextPage);
255238
fetchHackathons(nextPage, filters, true);
256-
} else {
257-
console.log(
258-
'[useHackathonsList] loadMore skipped (loadingMore or !hasMore)'
259-
);
260239
}
261240
}, [loadingMore, hasMore, currentPage, filters, fetchHackathons]);
262241

@@ -277,4 +256,4 @@ export function useHackathonsList(
277256
loadMore,
278257
refetch,
279258
};
280-
}
259+
};

hooks/use-infinite-scroll.ts

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,85 @@
11
'use client';
22

3-
import { useEffect } from 'react';
3+
import { useEffect, useRef } from 'react';
44

5-
interface UseInfiniteScrollOptions {
5+
export interface UseInfiniteScrollOptions {
66
onLoadMore: () => void;
77
hasMore: boolean;
88
loading: boolean;
99
rootMargin?: string;
10+
/** When true, logs observer lifecycle and skip reasons to console. Default: false. */
11+
debug?: boolean;
1012
}
1113

1214
/**
1315
* Triggers onLoadMore when the sentinel element enters the viewport (or within rootMargin of it).
1416
* Pass the sentinel DOM element (e.g. from a callback ref + state) so the observer attaches after mount.
17+
* onLoadMore does not need to be memoized (useCallback); a ref is used internally.
1518
*/
16-
export function useInfiniteScroll(
19+
export const useInfiniteScroll: (
1720
sentinel: HTMLElement | null,
1821
options: UseInfiniteScrollOptions
19-
): void {
22+
) => void = (sentinel, options) => {
2023
const {
2124
onLoadMore,
2225
hasMore,
2326
loading,
2427
rootMargin = '0px 0px 400px 0px',
28+
debug = false,
2529
} = options;
2630

31+
const onLoadMoreRef = useRef(onLoadMore);
32+
useEffect(() => {
33+
onLoadMoreRef.current = onLoadMore;
34+
}, [onLoadMore]);
35+
2736
useEffect(() => {
2837
if (!sentinel) {
29-
console.log(
30-
'[useInfiniteScroll] No sentinel element yet, skipping observer'
31-
);
38+
if (debug) {
39+
console.log(
40+
'[useInfiniteScroll] No sentinel element yet, skipping observer'
41+
);
42+
}
3243
return;
3344
}
3445

35-
console.log('[useInfiniteScroll] Observer attached to sentinel', {
36-
hasMore,
37-
loading,
38-
rootMargin,
39-
});
46+
if (debug) {
47+
console.log('[useInfiniteScroll] Observer attached to sentinel', {
48+
hasMore,
49+
loading,
50+
rootMargin,
51+
});
52+
}
4053

4154
const observer = new IntersectionObserver(
4255
entries => {
4356
const [entry] = entries;
4457
const isIntersecting = !!entry?.isIntersecting;
45-
console.log('[useInfiniteScroll] Observer callback', {
46-
isIntersecting,
47-
hasMore,
48-
loading,
49-
});
58+
if (debug) {
59+
console.log('[useInfiniteScroll] Observer callback', {
60+
isIntersecting,
61+
hasMore,
62+
loading,
63+
});
64+
}
5065

5166
if (!entry?.isIntersecting) return;
5267
if (!hasMore) {
53-
console.log('[useInfiniteScroll] Skipped: hasMore is false');
68+
if (debug) {
69+
console.log('[useInfiniteScroll] Skipped: hasMore is false');
70+
}
5471
return;
5572
}
5673
if (loading) {
57-
console.log('[useInfiniteScroll] Skipped: already loading');
74+
if (debug) {
75+
console.log('[useInfiniteScroll] Skipped: already loading');
76+
}
5877
return;
5978
}
60-
console.log('[useInfiniteScroll] Calling onLoadMore()');
61-
onLoadMore();
79+
if (debug) {
80+
console.log('[useInfiniteScroll] Calling onLoadMore()');
81+
}
82+
onLoadMoreRef.current?.();
6283
},
6384
{
6485
root: null,
@@ -69,8 +90,10 @@ export function useInfiniteScroll(
6990

7091
observer.observe(sentinel);
7192
return () => {
72-
console.log('[useInfiniteScroll] Observer disconnected');
93+
if (debug) {
94+
console.log('[useInfiniteScroll] Observer disconnected');
95+
}
7396
observer.disconnect();
7497
};
75-
}, [sentinel, onLoadMore, hasMore, loading, rootMargin]);
76-
}
98+
}, [sentinel, hasMore, loading, rootMargin, debug]);
99+
};

lib/api/hackathons/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ export const getPublicHackathonsList = async (
298298

299299
const pagination = raw.pagination;
300300
return {
301-
hackathons: raw.hackathons || [],
301+
hackathons: raw.hackathons ?? [],
302302
hasMore: pagination?.hasNext ?? false,
303303
total: pagination?.total ?? 0,
304304
currentPage: pagination?.page ?? 1,
5.26 KB
Loading
54.4 KB
Loading

0 commit comments

Comments
 (0)