From 37a74a2f67a0f84bec99e2f6f7cc231d8f2965d4 Mon Sep 17 00:00:00 2001 From: chiscookeke11 Date: Mon, 23 Feb 2026 23:11:26 +0100 Subject: [PATCH 1/4] implemented course fetch functionality --- src/app/coursesPage/courseExplore.tsx | 291 ++++++++++---------------- 1 file changed, 115 insertions(+), 176 deletions(-) diff --git a/src/app/coursesPage/courseExplore.tsx b/src/app/coursesPage/courseExplore.tsx index f1b644d..e1a2a8f 100644 --- a/src/app/coursesPage/courseExplore.tsx +++ b/src/app/coursesPage/courseExplore.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useState, useMemo } from "react"; +import React, { useState, useMemo, useEffect } from "react"; import { Search, Filter, X } from "lucide-react"; import CourseCard from "./components/courseCard"; import CreateCourse from "./components/createCourse"; @@ -7,176 +7,91 @@ import { levels } from "@/lib/interface"; import { categories } from "@/lib/interface"; import { Course, CourseLevel, CourseCategory } from "@/lib/interface"; -const coursesData: Course[] = [ - { - id: 1, - name: "React Fundamentals", - category: "Web Development", - level: "Beginner", - rating: 4.7, - students: 1250, - description: - "Learn the basics of React including components, state, and props. Perfect for beginners starting their journey.", - duration: "8 weeks", - price: 340.56, - }, - { - id: 2, - name: "Advanced JavaScript", - category: "Web Development", - level: "Advanced", - rating: 4.8, - students: 890, - description: - "Deep dive into advanced JavaScript concepts including closures, prototypes, and async programming.", - duration: "6 weeks", - price: 450.75, - }, - { - id: 3, - name: "Python for Data Science", - category: "Data Science", - level: "Intermediate", - rating: 4.6, - students: 2100, - description: - "Master Python programming for data analysis, visualization, and machine learning applications.", - duration: "10 weeks", - price: 520.3, - }, - { - id: 4, - name: "UI/UX Design Basics", - category: "Design & UI/UX", - level: "Beginner", - rating: 4.5, - students: 1580, - description: - "Learn the fundamentals of user interface and user experience design principles and tools.", - duration: "7 weeks", - price: 380.9, - }, - { - id: 5, - name: "Node.js Backend Development", - category: "Web Development", - level: "Intermediate", - rating: 4.7, - students: 940, - description: - "Build scalable backend applications using Node.js, Express, and MongoDB.", - duration: "9 weeks", - price: 410.25, - }, - { - id: 6, - name: "Machine Learning Fundamentals", - category: "Data Science", - level: "Advanced", - rating: 4.9, - students: 750, - description: - "Introduction to machine learning algorithms, neural networks, and practical applications.", - duration: "12 weeks", - price: 680.5, - }, - { - id: 7, - name: "Web Design with CSS", - category: "Design & UI/UX", - level: "Beginner", - rating: 4.4, - students: 1890, - description: - "Master CSS for creating beautiful, responsive web designs from scratch.", - duration: "5 weeks", - price: 290.4, - }, - { - id: 8, - name: "DevOps and Cloud Computing", - category: "DevOps & Cloud", - level: "Advanced", - rating: 4.6, - students: 620, - description: - "Learn DevOps practices, containerization, and cloud deployment strategies.", - duration: "11 weeks", - price: 590.8, - }, - { - id: 9, - name: "Data Visualization with D3.js", - category: "Data Science", - level: "Intermediate", - rating: 4.3, - students: 445, - description: - "Create interactive and compelling data visualizations using D3.js library and modern web standards.", - duration: "6 weeks", - price: 395.6, - }, - { - id: 10, - name: "Figma for Designers", - category: "Design & UI/UX", - level: "Beginner", - rating: 4.6, - students: 1320, - description: - "Master Figma for creating professional UI designs, prototypes, and design systems.", - duration: "4 weeks", - price: 275.8, - }, - { - id: 11, - name: "Docker & Kubernetes", - category: "DevOps & Cloud", - level: "Intermediate", - rating: 4.7, - students: 780, - description: - "Learn containerization with Docker and orchestration with Kubernetes for modern applications.", - duration: "8 weeks", - price: 510.25, - }, - { - id: 12, - name: "Full Stack Web Development", - category: "Web Development", - level: "Advanced", - rating: 4.8, - students: 960, - description: - "Complete full stack development course covering frontend, backend, and database technologies.", - duration: "16 weeks", - price: 799.99, - }, -]; + const CourseExplore: React.FC = () => { const [searchTerm, setSearchTerm] = useState(""); - const [selectedCategories, setSelectedCategories] = useState< - CourseCategory[] - >([]); + const [selectedCategories, setSelectedCategories] = useState([]); const [selectedLevels, setSelectedLevels] = useState([]); const [showMobileFilters, setShowMobileFilters] = useState(false); - - const filteredCourses = useMemo((): Course[] => { - return coursesData.filter((course: Course) => { - const matchesSearch: boolean = - (course.name?.toLowerCase().includes(searchTerm.toLowerCase()) || false) || - (course.description?.toLowerCase().includes(searchTerm.toLowerCase()) || false); + const [coursesData, setCoursesData] = useState([]) + const PAGE_SIZE = 10 + const [page, setPage] = useState(1) + const [hasMore, setHasMore] = useState(true) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + + + + // function to fetch courses + const fetchCourses = async (pageNumber: number) => { + try { + setLoading(true) + setError(null) + + const res = await fetch(`/api/courses?page=${pageNumber}&limit=${PAGE_SIZE}`); // replace with your backend URL + + if (!res.ok) throw new Error("Failed to fetch courses") + + const data: Course[] = await res.json() + + if (data.length < PAGE_SIZE) { + setHasMore(false) + } + + setCoursesData((prev) => [...prev, ...data]); + + } + + catch (error: unknown) { + if (error instanceof Error) { + setError(error.message) + } + else { + setError("Something went wrong") + } + } + + finally { + setLoading(false) + } + } + + + // Initial + Pagination Fetch + useEffect(() => { + fetchCourses(page) + }, [page]) - const matchesCategory: boolean = - selectedCategories.length === 0 || (course.category ? selectedCategories.includes(course.category) : false); - const matchesLevel: boolean = - selectedLevels.length === 0 || (course.level ? selectedLevels.includes(course.level) : false); + + + // filter logic + const filteredCourses = useMemo((): Course[] => { + return coursesData.filter((course: Course) => { + const matchesSearch: boolean = + (course.name?.toLowerCase().includes(searchTerm.toLowerCase()) || false) || + (course.description?.toLowerCase().includes(searchTerm.toLowerCase()) || false); + + const matchesCategory: boolean = + selectedCategories.length === 0 || (course.category ? selectedCategories.includes(course.category) : false); + + const matchesLevel: boolean = + selectedLevels.length === 0 || (course.level ? selectedLevels.includes(course.level) : false); return matchesSearch && matchesCategory && matchesLevel; }); - }, [searchTerm, selectedCategories, selectedLevels]); + }, [searchTerm, selectedCategories, selectedLevels, coursesData]); + + + // function to load more courses + const loadMore = () => { + if (hasMore && !loading) { + setPage((prev) => prev + 1); + } + } + const handleCategoryChange = (category: CourseCategory): void => { setSelectedCategories((prev: CourseCategory[]) => @@ -368,24 +283,48 @@ const CourseExplore: React.FC = () => { )}
- {filteredCourses.length === 0 ? ( -
-

- No courses found matching your criteria. -

-

- Try adjusting your filters or search term. -

-
- ) : ( -
- {filteredCourses.map((course: Course) => ( - - ))} + {loading ? ( +
+ Loading courses... +
) + : + !loading && filteredCourses.length === 0 ? ( +
+

+ No courses found matching your criteria. +

+

+ Try adjusting your filters or search term. +

+
+ ) : ( +
+ {filteredCourses.map((course: Course) => ( + + ))} + +
+ )} + + {hasMore && ( +
+ { + coursesData.length > 0 ? ( + + ) + : + null + }
)} -
+
Showing {filteredCourses.length} of {coursesData.length} courses
From 2eef4b3a24ff47797a67c00c0d2b6fa0214fd8fb Mon Sep 17 00:00:00 2001 From: chiscookeke11 Date: Tue, 24 Feb 2026 05:42:21 +0100 Subject: [PATCH 2/4] implemented the Course Listing and Details Pages from backend endpoint --- src/app/coursesPage/[id]/page.tsx | 61 +++ src/app/coursesPage/components/courseCard.tsx | 13 +- src/app/coursesPage/courseExplore.tsx | 412 +++++++----------- 3 files changed, 222 insertions(+), 264 deletions(-) create mode 100644 src/app/coursesPage/[id]/page.tsx diff --git a/src/app/coursesPage/[id]/page.tsx b/src/app/coursesPage/[id]/page.tsx new file mode 100644 index 0000000..c3ebc92 --- /dev/null +++ b/src/app/coursesPage/[id]/page.tsx @@ -0,0 +1,61 @@ +"use client" + + +import React, { useEffect, useState } from "react"; +import { useParams } from "next/navigation"; + +interface CourseDetail { + id: string; + title: string; + description: string; +} + +const CourseDetailPage = () => { + const params = useParams(); + const { id } = params; + const [course, setCourse] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + const fetchCourse = async () => { + try { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/courses/${id}`); + if (res.status === 404) { + setError(true); + return; + } + const data = await res.json(); + setCourse(data); + } catch (err) { + console.error(err); + setError(true); + } finally { + setLoading(false); + } + }; + + fetchCourse(); + }, [id]); + + if (loading) return ( +
+

Loading course...

+
+ ); + + + if (error || !course) return + ( +

Course not found.

+ ); + + return ( +
+

{course.title}

+

{course.description}

+
+ ); +}; + +export default CourseDetailPage; \ No newline at end of file diff --git a/src/app/coursesPage/components/courseCard.tsx b/src/app/coursesPage/components/courseCard.tsx index 1044e19..2bd40cb 100644 --- a/src/app/coursesPage/components/courseCard.tsx +++ b/src/app/coursesPage/components/courseCard.tsx @@ -3,6 +3,7 @@ import { CourseCardProps, CourseCategory } from "@/lib/interface"; import { Star, Clock } from "lucide-react"; import { useState } from "react"; import { grantAccess } from "../../../../contract_connections/CourseRegistry/grantAccess"; +import Link from "next/link"; const CourseCard: React.FC = ({ course }) => { const [userAddress] = useState("0x1234567890123456789012345678901234567890"); @@ -96,12 +97,18 @@ const CourseCard: React.FC = ({ course }) => { {course.price ? (typeof course.price === 'number' ? course.price.toFixed(2) : course.price) : 'Free'} XLM - + + +
diff --git a/src/app/coursesPage/courseExplore.tsx b/src/app/coursesPage/courseExplore.tsx index e1a2a8f..5117d67 100644 --- a/src/app/coursesPage/courseExplore.tsx +++ b/src/app/coursesPage/courseExplore.tsx @@ -1,5 +1,6 @@ "use client"; -import React, { useState, useMemo, useEffect } from "react"; + +import React, { useState, useMemo, useEffect, useRef } from "react"; import { Search, Filter, X } from "lucide-react"; import CourseCard from "./components/courseCard"; import CreateCourse from "./components/createCourse"; @@ -7,111 +8,157 @@ import { levels } from "@/lib/interface"; import { categories } from "@/lib/interface"; import { Course, CourseLevel, CourseCategory } from "@/lib/interface"; - - const CourseExplore: React.FC = () => { const [searchTerm, setSearchTerm] = useState(""); const [selectedCategories, setSelectedCategories] = useState([]); const [selectedLevels, setSelectedLevels] = useState([]); const [showMobileFilters, setShowMobileFilters] = useState(false); - const [coursesData, setCoursesData] = useState([]) - const PAGE_SIZE = 10 - const [page, setPage] = useState(1) - const [hasMore, setHasMore] = useState(true) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - - - - // function to fetch courses + const [coursesData, setCoursesData] = useState([]); + const PAGE_SIZE = 10; + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [initialLoading, setInitialLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [error, setError] = useState(null); + const observerRef = useRef(null); + + /** + * Fetches courses based on the page number. + * - Page 1 → triggers full-page loading state + * - Page > 1 → triggers pagination loading state + */ const fetchCourses = async (pageNumber: number) => { try { - setLoading(true) - setError(null) + if (pageNumber === 1) { + setInitialLoading(true); + } else { + setLoadingMore(true); + } - const res = await fetch(`/api/courses?page=${pageNumber}&limit=${PAGE_SIZE}`); // replace with your backend URL + setError(null); - if (!res.ok) throw new Error("Failed to fetch courses") + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/courses?page=${pageNumber}&limit=${PAGE_SIZE}` + ); - const data: Course[] = await res.json() + if (!res.ok) throw new Error("Failed to fetch courses"); + const data: Course[] = await res.json(); + + /** + * If fewer than PAGE_SIZE returned, + * there are no more pages available. + */ if (data.length < PAGE_SIZE) { - setHasMore(false) + setHasMore(false); } - setCoursesData((prev) => [...prev, ...data]); - - } - - catch (error: unknown) { + /** + * Prevent duplicate entries: + * Only append courses that do not already exist in state. + */ + setCoursesData((prev) => { + const existingIds = new Set(prev.map((c) => c.id)); + const filtered = data.filter((c) => !existingIds.has(c.id)); + return [...prev, ...filtered]; + }); + } catch (error: unknown) { if (error instanceof Error) { - setError(error.message) - } - else { - setError("Something went wrong") + setError(error.message); + } else { + setError("Something went wrong"); } + } finally { + setInitialLoading(false); + setLoadingMore(false); } + }; - finally { - setLoading(false) - } - } - - - // Initial + Pagination Fetch + /** + * Fetch on page change. + * This drives both initial load and pagination. + */ useEffect(() => { - fetchCourses(page) - }, [page]) - - - - - // filter logic + fetchCourses(page); + }, [page]); + + /** + * Filters based on: + * - Search term + * - Selected categories + * - Selected levels + */ const filteredCourses = useMemo((): Course[] => { return coursesData.filter((course: Course) => { - const matchesSearch: boolean = + const matchesSearch = (course.name?.toLowerCase().includes(searchTerm.toLowerCase()) || false) || (course.description?.toLowerCase().includes(searchTerm.toLowerCase()) || false); - const matchesCategory: boolean = - selectedCategories.length === 0 || (course.category ? selectedCategories.includes(course.category) : false); + const matchesCategory = + selectedCategories.length === 0 || + (course.category ? selectedCategories.includes(course.category) : false); - const matchesLevel: boolean = - selectedLevels.length === 0 || (course.level ? selectedLevels.includes(course.level) : false); + const matchesLevel = + selectedLevels.length === 0 || + (course.level ? selectedLevels.includes(course.level) : false); return matchesSearch && matchesCategory && matchesLevel; }); }, [searchTerm, selectedCategories, selectedLevels, coursesData]); + /** + * Using IntersectionObserver to detect when the sentinel div + * enters the viewport and increments page number. + * + * Guard conditions: + * - Do nothing if no more pages + * - Do nothing if already loading + */ + useEffect(() => { + if (!hasMore || loadingMore) return; + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setPage((prev) => prev + 1); + } + }, + { threshold: 1 } + ); - // function to load more courses - const loadMore = () => { - if (hasMore && !loading) { - setPage((prev) => prev + 1); + if (observerRef.current) { + observer.observe(observerRef.current); } - } - + return () => { + if (observerRef.current) { + observer.unobserve(observerRef.current); + } + }; + }, [hasMore, loadingMore]); + + /** + * ========================= + * HANDLERS + * ========================= + */ const handleCategoryChange = (category: CourseCategory): void => { - setSelectedCategories((prev: CourseCategory[]) => + setSelectedCategories((prev) => prev.includes(category) - ? prev.filter((c: CourseCategory) => c !== category) + ? prev.filter((c) => c !== category) : [...prev, category] ); }; const handleLevelChange = (level: CourseLevel): void => { - setSelectedLevels((prev: CourseLevel[]) => + setSelectedLevels((prev) => prev.includes(level) - ? prev.filter((l: CourseLevel) => l !== level) + ? prev.filter((l) => l !== level) : [...prev, level] ); }; - const handleSearchChange = ( - event: React.ChangeEvent - ): void => { + const handleSearchChange = (event: React.ChangeEvent): void => { setSearchTerm(event.target.value); }; @@ -119,219 +166,62 @@ const CourseExplore: React.FC = () => { setShowMobileFilters(!showMobileFilters); }; + return (
-
-

- Explore courses -

-
- -
- - -
+ {/* Initial Loading State */} + {initialLoading ? ( +
+ Loading courses... +
-
- -
- -
- -
-
-
-
-

Filters

- -
- -

- Filters -

- -
-

Categories

-
- {categories.map((category: CourseCategory) => ( - - ))} -
-
- -
-

Level

-
- {levels.map((level: CourseLevel) => ( - - ))} -
-
- - {(selectedCategories.length > 0 || selectedLevels.length > 0) && ( -
- -
- )} -
+ ) : filteredCourses.length === 0 ? ( +
+ No courses found matching your criteria.
+ ) : ( +
+ {filteredCourses.map((course: Course) => ( + + ))} +
+ )} - {showMobileFilters && ( -
- )} - -
- {loading ? ( -
- Loading courses... -
) - : - !loading && filteredCourses.length === 0 ? ( -
-

- No courses found matching your criteria. -

-

- Try adjusting your filters or search term. -

-
- ) : ( -
- {filteredCourses.map((course: Course) => ( - - ))} - -
- )} + {/* Pagination Loader (appears below existing courses) */} + {!initialLoading && loadingMore && ( +
+ +
+ )} - {hasMore && ( -
- { - coursesData.length > 0 ? ( - - ) - : - null - } -
- )} + {/* Infinite Scroll Sentinel */} + {hasMore &&
} -
- Showing {filteredCourses.length} of {coursesData.length} courses -
+ {/* Footer Info */} + {!initialLoading && !loadingMore && ( +
+ Showing {filteredCourses.length} of {coursesData.length} courses
-
+ )}
); }; export default CourseExplore; + +/** + * Simple animated loader used for both + * initial loading and pagination loading. + */ +const Loader = () => { + return ( + + + + + + ); +}; \ No newline at end of file From 6ebf92f64d39ad82b13338651a90b4a906ebbc4b Mon Sep 17 00:00:00 2001 From: chiscookeke11 Date: Tue, 24 Feb 2026 05:58:23 +0100 Subject: [PATCH 3/4] UI fix --- src/app/coursesPage/courseExplore.tsx | 431 +++++++++++++++++--------- 1 file changed, 292 insertions(+), 139 deletions(-) diff --git a/src/app/coursesPage/courseExplore.tsx b/src/app/coursesPage/courseExplore.tsx index 5117d67..143cf4e 100644 --- a/src/app/coursesPage/courseExplore.tsx +++ b/src/app/coursesPage/courseExplore.tsx @@ -1,6 +1,5 @@ "use client"; - -import React, { useState, useMemo, useEffect, useRef } from "react"; +import React, { useState, useMemo, useEffect, useRef, use } from "react"; import { Search, Filter, X } from "lucide-react"; import CourseCard from "./components/courseCard"; import CreateCourse from "./components/createCourse"; @@ -8,157 +7,113 @@ import { levels } from "@/lib/interface"; import { categories } from "@/lib/interface"; import { Course, CourseLevel, CourseCategory } from "@/lib/interface"; + + const CourseExplore: React.FC = () => { const [searchTerm, setSearchTerm] = useState(""); const [selectedCategories, setSelectedCategories] = useState([]); const [selectedLevels, setSelectedLevels] = useState([]); const [showMobileFilters, setShowMobileFilters] = useState(false); - const [coursesData, setCoursesData] = useState([]); - const PAGE_SIZE = 10; - const [page, setPage] = useState(1); - const [hasMore, setHasMore] = useState(true); - const [initialLoading, setInitialLoading] = useState(true); - const [loadingMore, setLoadingMore] = useState(false); - const [error, setError] = useState(null); - const observerRef = useRef(null); - - /** - * Fetches courses based on the page number. - * - Page 1 → triggers full-page loading state - * - Page > 1 → triggers pagination loading state - */ + const [coursesData, setCoursesData] = useState([]) + const PAGE_SIZE = 10 + const [page, setPage] = useState(1) + const [hasMore, setHasMore] = useState(true) + const [initialLoading, setInitialLoading] = useState(true) + const [loadingMore, setLoadingMore] = useState(false) + const [error, setError] = useState(null) + const observerRef = useRef(null) + + + + + // function to fetch courses const fetchCourses = async (pageNumber: number) => { try { if (pageNumber === 1) { - setInitialLoading(true); + setInitialLoading(true) } else { - setLoadingMore(true); + setLoadingMore(true) } - setError(null); + setError(null) const res = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/courses?page=${pageNumber}&limit=${PAGE_SIZE}` - ); + ) - if (!res.ok) throw new Error("Failed to fetch courses"); + if (!res.ok) throw new Error("Failed to fetch courses") - const data: Course[] = await res.json(); + const data: Course[] = await res.json() - /** - * If fewer than PAGE_SIZE returned, - * there are no more pages available. - */ if (data.length < PAGE_SIZE) { - setHasMore(false); + setHasMore(false) } - /** - * Prevent duplicate entries: - * Only append courses that do not already exist in state. - */ - setCoursesData((prev) => { - const existingIds = new Set(prev.map((c) => c.id)); - const filtered = data.filter((c) => !existingIds.has(c.id)); - return [...prev, ...filtered]; - }); + setCoursesData(prev => { + const existingIds = new Set(prev.map(c => c.id)) + const filtered = data.filter(c => !existingIds.has(c.id)) + return [...prev, ...filtered] + }) + } catch (error: unknown) { if (error instanceof Error) { - setError(error.message); + setError(error.message) } else { - setError("Something went wrong"); + setError("Something went wrong") } } finally { - setInitialLoading(false); - setLoadingMore(false); + setInitialLoading(false) + setLoadingMore(false) } - }; + } + - /** - * Fetch on page change. - * This drives both initial load and pagination. - */ + // Initial + Pagination Fetch useEffect(() => { - fetchCourses(page); - }, [page]); - - /** - * Filters based on: - * - Search term - * - Selected categories - * - Selected levels - */ + fetchCourses(page) + }, [page]) + + + + + // filter logic const filteredCourses = useMemo((): Course[] => { return coursesData.filter((course: Course) => { - const matchesSearch = + const matchesSearch: boolean = (course.name?.toLowerCase().includes(searchTerm.toLowerCase()) || false) || (course.description?.toLowerCase().includes(searchTerm.toLowerCase()) || false); - const matchesCategory = - selectedCategories.length === 0 || - (course.category ? selectedCategories.includes(course.category) : false); + const matchesCategory: boolean = + selectedCategories.length === 0 || (course.category ? selectedCategories.includes(course.category) : false); - const matchesLevel = - selectedLevels.length === 0 || - (course.level ? selectedLevels.includes(course.level) : false); + const matchesLevel: boolean = + selectedLevels.length === 0 || (course.level ? selectedLevels.includes(course.level) : false); return matchesSearch && matchesCategory && matchesLevel; }); }, [searchTerm, selectedCategories, selectedLevels, coursesData]); - /** - * Using IntersectionObserver to detect when the sentinel div - * enters the viewport and increments page number. - * - * Guard conditions: - * - Do nothing if no more pages - * - Do nothing if already loading - */ - useEffect(() => { - if (!hasMore || loadingMore) return; - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) { - setPage((prev) => prev + 1); - } - }, - { threshold: 1 } - ); - if (observerRef.current) { - observer.observe(observerRef.current); - } - - return () => { - if (observerRef.current) { - observer.unobserve(observerRef.current); - } - }; - }, [hasMore, loadingMore]); - - /** - * ========================= - * HANDLERS - * ========================= - */ const handleCategoryChange = (category: CourseCategory): void => { - setSelectedCategories((prev) => + setSelectedCategories((prev: CourseCategory[]) => prev.includes(category) - ? prev.filter((c) => c !== category) + ? prev.filter((c: CourseCategory) => c !== category) : [...prev, category] ); }; const handleLevelChange = (level: CourseLevel): void => { - setSelectedLevels((prev) => + setSelectedLevels((prev: CourseLevel[]) => prev.includes(level) - ? prev.filter((l) => l !== level) + ? prev.filter((l: CourseLevel) => l !== level) : [...prev, level] ); }; - const handleSearchChange = (event: React.ChangeEvent): void => { + const handleSearchChange = ( + event: React.ChangeEvent + ): void => { setSearchTerm(event.target.value); }; @@ -167,44 +122,241 @@ const CourseExplore: React.FC = () => { }; + + // this useEffect loads more course once the sentinel div comes into view + useEffect(() => { + if (!hasMore || loadingMore) return + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setPage(prev => prev + 1) + } + }, + { threshold: 1 } + ) + + if (observerRef.current) { + observer.observe(observerRef.current) + } + + return () => { + if (observerRef.current) { + observer.unobserve(observerRef.current) + } + } + }, [hasMore, loadingMore]) + + return (
+
+

+ Explore courses +

- {/* Initial Loading State */} - {initialLoading ? ( -
- Loading courses... - +
+ +
+ + +
- ) : filteredCourses.length === 0 ? ( -
- No courses found matching your criteria. -
- ) : ( -
- {filteredCourses.map((course: Course) => ( - - ))} -
- )} +
+ +
+ +
+ +
+
+
+
+

Filters

+ +
- {/* Pagination Loader (appears below existing courses) */} - {!initialLoading && loadingMore && ( -
- +

+ Filters +

+ +
+

Categories

+
+ {categories.map((category: CourseCategory) => ( + + ))} +
+
+ +
+

Level

+
+ {levels.map((level: CourseLevel) => ( + + ))} +
+
+ + {(selectedCategories.length > 0 || selectedLevels.length > 0) && ( +
+ +
+ )} +
- )} - {/* Infinite Scroll Sentinel */} - {hasMore &&
} + {showMobileFilters && ( +
+ )} + +
+ {initialLoading ? ( +
+ Loading courses... + +
) + : error ? +
+

{error}

+
+ : + !initialLoading && filteredCourses.length === 0 ? ( +
+

+ No courses found matching your criteria. +

+

+ Try adjusting your filters or search term. +

+
+ ) : ( +
+ {filteredCourses.map((course: Course) => ( + + ))} - {/* Footer Info */} - {!initialLoading && !loadingMore && ( -
- Showing {filteredCourses.length} of {coursesData.length} courses +
+ )} + + {!initialLoading && loadingMore && ( +
+ +
+ )} + + {hasMore && ( +
+ )} + + {!initialLoading && !loadingMore && ( +
+ Showing {filteredCourses.length} of {coursesData.length} courses +
+ )}
- )} +
); @@ -212,16 +364,17 @@ const CourseExplore: React.FC = () => { export default CourseExplore; -/** - * Simple animated loader used for both - * initial loading and pagination loading. - */ + + + const Loader = () => { return ( - - - - - - ); -}; \ No newline at end of file + <> + + + + + + + ) +} \ No newline at end of file From 9114e48f7636b1cd2a76e019fe2d8dbde0128a0d Mon Sep 17 00:00:00 2001 From: chiscookeke11 Date: Tue, 24 Feb 2026 06:10:16 +0100 Subject: [PATCH 4/4] handled incorrect course ID in dynamic page --- src/app/coursesPage/[id]/page.tsx | 121 +++++++++++++++++++----------- 1 file changed, 77 insertions(+), 44 deletions(-) diff --git a/src/app/coursesPage/[id]/page.tsx b/src/app/coursesPage/[id]/page.tsx index c3ebc92..708b031 100644 --- a/src/app/coursesPage/[id]/page.tsx +++ b/src/app/coursesPage/[id]/page.tsx @@ -1,61 +1,94 @@ -"use client" - +"use client"; import React, { useEffect, useState } from "react"; import { useParams } from "next/navigation"; interface CourseDetail { - id: string; - title: string; - description: string; + id: string; + title: string; + description: string; } const CourseDetailPage = () => { - const params = useParams(); - const { id } = params; - const [course, setCourse] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); - - useEffect(() => { - const fetchCourse = async () => { - try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/courses/${id}`); - if (res.status === 404) { - setError(true); - return; - } - const data = await res.json(); - setCourse(data); - } catch (err) { - console.error(err); - setError(true); - } finally { - setLoading(false); - } - }; - - fetchCourse(); - }, [id]); - - if (loading) return ( -
-

Loading course...

-
- ); + const params = useParams(); + const id = params?.id as string | undefined; + + const [course, setCourse] = useState(null); + const [loading, setLoading] = useState(true); + const [notFound, setNotFound] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!id) { + setNotFound(true); + setLoading(false); + return; + } + + const fetchCourse = async () => { + try { + setLoading(true); + setError(null); + setNotFound(false); + + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/courses/${id}` + ); + + if (res.status === 404) { + setNotFound(true); + return; + } + + if (!res.ok) { + throw new Error("Failed to fetch course"); + } + const data: CourseDetail = await res.json(); + setCourse(data); + } catch (err) { + console.error(err); + setError("Something went wrong while loading the course."); + } finally { + setLoading(false); + } + }; - if (error || !course) return - ( -

Course not found.

+ fetchCourse(); + }, [id]); + + + + if (loading) { + return ( +
+

Loading course...

+
); + } + if (notFound) { return ( -
-

{course.title}

-

{course.description}

-
+
+

Course not found.

+
); + } + + if (error) { + return ( +
+

{error}

+
+ ); + } + + return ( +
+

{course?.title}

+

{course?.description}

+
+ ); }; export default CourseDetailPage; \ No newline at end of file