diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index c43a0e54..fb0758b2 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { motion } from "framer-motion"; -import { useProjects } from "@/lib/queries/project/list-projects"; +import { useProjectsInfinite } from "@/lib/queries/project/list-projects"; // UPDATED IMPORT import { Skeleton } from "@/components/ui/skeleton"; import { CreateProjectDialog } from "@/components/project/create-project-dialog"; import { ProjectCard } from "@/components/project/project-card"; @@ -10,7 +10,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { updateProject } from "@/lib/mutations/project/update-project"; import { deleteProject } from "@/lib/mutations/project/delete-project"; import { useToast } from "@/hooks/use-toast"; -import { FolderIcon, SettingsIcon } from "lucide-react"; +import { FolderIcon, SettingsIcon, AlertCircle, Loader2 } from "lucide-react"; // Added Loader2 import { ProtectedPage } from "@/components/auth/protected-page"; import { AuthStatus } from "@/components/auth/auth-status"; import { MentionInput } from "@/components/chat/mention-input"; @@ -19,27 +19,36 @@ import { useState } from "react"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { AlertCircle } from "lucide-react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { ThemeToggle } from "@/components/theme/toggle"; import { ContextSelectionHelper } from "@/components/chat/context-selection-helper"; -// import { UserInfo } from "@/components/dashboard/user-info"; +import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; // NEW IMPORT export default function HomePage() { const { toast } = useToast(); const queryClient = useQueryClient(); const router = useRouter(); const { - data: projects, - isLoading, - error, - } = useProjects({ - variables: { - limit: 100, - page: 1, - }, + data: projectsData, + isLoading: isProjectsLoading, + error: projectsError, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useProjectsInfinite(12); // Load 12 projects per page + + // Infinite Scroll Trigger + const { ref: loadMoreRef, inView } = useIntersectionObserver({ + threshold: 0.1, + enabled: hasNextPage && !isFetchingNextPage, }); + React.useEffect(() => { + if (inView && hasNextPage) { + fetchNextPage(); + } + }, [inView, hasNextPage, fetchNextPage]); + // Chat functionality const [inputValue, setInputValue] = useState(""); const [selectedContexts, setSelectedContexts] = useState([]); @@ -167,14 +176,14 @@ export default function HomePage() { }; const renderContent = () => { - if (error) { + if (projectsError) { return (
Error - Failed to load projects: {error.message} + Failed to load projects: {projectsError.message} @@ -182,7 +191,7 @@ export default function HomePage() { ); } - if (isLoading) { + if (isProjectsLoading) { return (
@@ -198,6 +207,9 @@ export default function HomePage() { ); } + // Flatten pages into single list + const allProjects = projectsData?.pages.flatMap((page) => page.results || []) || []; + return (
{/* Gradient background */} @@ -332,7 +344,7 @@ export default function HomePage() {
- {/* Projects section pushed to bottom */} + {/* Projects section */}
- - {projects && projects.results && projects.results.length > 0 ? ( - projects.results.map((project, idx) => ( - - - - )) - ) : ( + {allProjects.length > 0 ? ( +
-
- -
-

- No projects yet -

-

- Start by creating your first project to organize your data -

- + {allProjects.map((project, idx) => ( + + + + ))}
- )} - + + {/* Loading Trigger */} +
+ {isFetchingNextPage && ( + + )} +
+
+ ) : ( + +
+ +
+

+ No projects yet +

+

+ Start by creating your first project to organize your data +

+ +
+ )}
diff --git a/web/src/app/projects/[projectId]/page.tsx b/web/src/app/projects/[projectId]/page.tsx index 077a6a2a..f9470046 100644 --- a/web/src/app/projects/[projectId]/page.tsx +++ b/web/src/app/projects/[projectId]/page.tsx @@ -3,17 +3,18 @@ import * as React from "react"; import { useProject } from "@/lib/queries/project/get-project"; import { Skeleton } from "@/components/ui/skeleton"; -import { TableIcon, UploadIcon } from "lucide-react"; +import { TableIcon, UploadIcon, Loader2 } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { DatasetCard } from "@/components/dataset/dataset-card"; import { motion } from "framer-motion"; -import { useDatasets } from "@/lib/queries/dataset/list-datasets"; +import { useDatasetsInfinite } from "@/lib/queries/dataset/list-datasets"; // UPDATED import { deleteDataset } from "@/lib/mutations/dataset/delete-dataset"; import { useToast } from "@/hooks/use-toast"; import { useQueryClient } from "@tanstack/react-query"; import { InlineProjectEditor } from "@/components/project/inline-project-editor"; import Link from "next/link"; +import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; // YOUR NEW HOOK export default function ProjectPage({ params, @@ -24,23 +25,38 @@ export default function ProjectPage({ const { toast } = useToast(); const queryClient = useQueryClient(); const [isMounted, setIsMounted] = React.useState(false); - + const { data: project, - isLoading, - error, + isLoading: isProjectLoading, + error: projectError, } = useProject({ variables: { projectId, }, }); - const { data: datasets } = useDatasets({ - variables: { - projectId, - }, + // Use Infinite Query + const { + data: datasetsData, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading: isDatasetsLoading, + } = useDatasetsInfinite(projectId, 12); + + // Use Custom Intersection Observer Hook + const { ref: loadMoreRef, inView } = useIntersectionObserver({ + threshold: 0.1, + enabled: hasNextPage && !isFetchingNextPage, }); + React.useEffect(() => { + if (inView && hasNextPage) { + fetchNextPage(); + } + }, [inView, hasNextPage, fetchNextPage]); + React.useEffect(() => { setIsMounted(true); }, []); @@ -48,8 +64,9 @@ export default function ProjectPage({ const handleDeleteDataset = async (datasetId: string) => { try { await deleteDataset(projectId, datasetId); + // Invalidate specifically the infinite query await queryClient.invalidateQueries({ - queryKey: ["datasets"], + queryKey: ["datasets", projectId, "infinite"], }); toast({ title: "Dataset deleted", @@ -65,12 +82,9 @@ export default function ProjectPage({ } }; - // Return null during SSR and initial client render to avoid hydration mismatch - if (!isMounted) { - return null; - } + if (!isMounted) return null; - if (isLoading) { + if (isProjectLoading) { return (
@@ -98,12 +112,12 @@ export default function ProjectPage({ ); } - if (error) { + if (projectError) { return (

Error

-

{error.message}

+

{projectError.message}

); @@ -111,6 +125,10 @@ export default function ProjectPage({ if (!project) return null; + // Flatten the pages + const allDatasets = datasetsData?.pages.flatMap((page) => page.results || []) || []; + const totalDatasets = datasetsData?.pages[0]?.total || 0; + return (
@@ -120,7 +138,7 @@ export default function ProjectPage({

Datasets - {datasets?.total || 0} + {totalDatasets}

@@ -131,7 +149,7 @@ export default function ProjectPage({
- {datasets?.total === 0 ? ( + {allDatasets.length === 0 && !isDatasetsLoading ? ( ) : ( - - {datasets?.results?.map((dataset, idx) => ( - - - - ))} - +
+ + {allDatasets.map((dataset, idx) => ( + + + + ))} + + + {/* Infinite Scroll Trigger */} +
+ {isFetchingNextPage && ( + + )} +
+
)}
); -} +} \ No newline at end of file diff --git a/web/src/hooks/use-intersection-observer.ts b/web/src/hooks/use-intersection-observer.ts new file mode 100644 index 00000000..c474b026 --- /dev/null +++ b/web/src/hooks/use-intersection-observer.ts @@ -0,0 +1,43 @@ +import { useEffect, useRef, useState } from "react"; + +interface UseIntersectionObserverProps { + threshold?: number; + root?: Element | null; + rootMargin?: string; + enabled?: boolean; +} + +export function useIntersectionObserver({ + threshold = 0, + root = null, + rootMargin = "0%", + enabled = true, +}: UseIntersectionObserverProps = {}) { + const ref = useRef(null); + const [inView, setInView] = useState(false); + + useEffect(() => { + const element = ref.current; + if (!element || !enabled) return; + + const observer = new IntersectionObserver( + ([entry]) => { + setInView(entry.isIntersecting); + }, + { + threshold, + root, + rootMargin, + } + ); + + observer.observe(element); + + return () => { + observer.disconnect(); + setInView(false); // Reset state on cleanup + }; + }, [threshold, root, rootMargin, enabled]); + + return { ref, inView }; +} \ No newline at end of file diff --git a/web/src/lib/queries/dataset/list-datasets.ts b/web/src/lib/queries/dataset/list-datasets.ts index 9e9c53bd..809e2ff1 100644 --- a/web/src/lib/queries/dataset/list-datasets.ts +++ b/web/src/lib/queries/dataset/list-datasets.ts @@ -1,7 +1,7 @@ import { Dataset, PaginatedResponse } from "@/lib/api-client"; import { apiClient } from "@/lib/api-client"; import { createQuery } from "react-query-kit"; - +import { useInfiniteQuery } from "@tanstack/react-query"; interface ListDatasetsParams { projectId: string; limit?: number; @@ -46,3 +46,27 @@ export const useDatasets = createQuery({ queryKey: ["datasets"], fetcher: fetchDatasets, }); + +export const useDatasetsInfinite = (projectId: string, limit = 12) => { + return useInfiniteQuery, Error>({ + queryKey: ["datasets", projectId, "infinite"], + initialPageParam: 1, + enabled: !!projectId, + queryFn: ({ pageParam }) => + fetchDatasets({ + projectId, + limit, + page: pageParam as number + }), + getNextPageParam: (lastPage, allPages) => { + // Handle null results + const results = lastPage.results || []; + // If results are empty or less than limit, we are done + if (results.length < limit) return undefined; + + const totalPages = Math.ceil(lastPage.total / limit); + const nextPage = allPages.length + 1; + return nextPage <= totalPages ? nextPage : undefined; + }, + }); +}; \ No newline at end of file diff --git a/web/src/lib/queries/project/list-projects.ts b/web/src/lib/queries/project/list-projects.ts index 3d6df971..36fe5c6c 100644 --- a/web/src/lib/queries/project/list-projects.ts +++ b/web/src/lib/queries/project/list-projects.ts @@ -1,6 +1,7 @@ import { Project, PaginatedResponse } from "@/lib/api-client"; import { apiClient } from "@/lib/api-client"; import { createQuery } from "react-query-kit"; +import { useInfiniteQuery } from "@tanstack/react-query"; interface ListProjectsParams { limit?: number; @@ -34,3 +35,24 @@ export const useProjects = createQuery({ queryKey: ["projects"], fetcher: fetchProjects, }); + +export const useProjectsInfinite = (limit = 12) => { + return useInfiniteQuery, Error>({ + queryKey: ["projects", "infinite"], + initialPageParam: 1, + queryFn: ({ pageParam }) => + fetchProjects({ + limit, + page: pageParam as number + }), + getNextPageParam: (lastPage, allPages) => { + // Handle null results + const results = lastPage.results || []; + + if (results.length < limit) return undefined; + const totalPages = Math.ceil(lastPage.total / limit); + const nextPage = allPages.length + 1; + return nextPage <= totalPages ? nextPage : undefined; + }, + }); +};