diff --git a/src/hooks/useImageGallery.js b/src/hooks/useImageGallery.js index 7e8e5334..b1511d93 100644 --- a/src/hooks/useImageGallery.js +++ b/src/hooks/useImageGallery.js @@ -2,84 +2,115 @@ import { useState, useCallback } from "react"; // import { ImageGalleryAPI } from "../repositories/ImageGalleyRepository"; import { ImageGalleryAPI } from "../repositories/ImageGalleryAPI"; +// const PAGE_SIZE = 10; + +// const useImageGallery = (selectedProjectId) => { +// const [images, setImages] = useState([]); +// const [allImagesData, setAllImagesData] = useState([]); +// const [pageNumber, setPageNumber] = useState(1); +// const [hasMore, setHasMore] = useState(true); +// const [loading, setLoading] = useState(false); +// const [loadingMore, setLoadingMore] = useState(false); + +// const fetchImages = useCallback(async (page = 1, filters = {}, reset = false) => { +// if (!selectedProjectId) return; + +// try { +// if (page === 1) { +// setLoading(true); +// } else { +// setLoadingMore(true); +// } + +// const res = await ImageGalleryAPI.ImagesGet( +// selectedProjectId, +// filters, +// page, +// PAGE_SIZE +// ); + +// const newBatches = res.data || []; +// const receivedCount = newBatches.length; + +// setImages((prev) => { +// if (page === 1 || reset) return newBatches; +// const uniqueNew = newBatches.filter( +// (batch) => !prev.some((b) => b.batchId === batch.batchId) +// ); +// return [...prev, ...uniqueNew]; +// }); + +// setAllImagesData((prev) => { +// if (page === 1 || reset) return newBatches; +// const uniqueAll = newBatches.filter( +// (batch) => !prev.some((b) => b.batchId === batch.batchId) +// ); +// return [...prev, ...uniqueAll]; +// }); + +// setHasMore(receivedCount === PAGE_SIZE); +// } catch (error) { +// console.error("Error fetching images:", error); +// if (page === 1) { +// setImages([]); +// setAllImagesData([]); +// } +// setHasMore(false); +// } finally { +// setLoading(false); +// setLoadingMore(false); +// } +// }, [selectedProjectId]); + +// const resetGallery = useCallback(() => { +// setImages([]); +// setAllImagesData([]); +// setPageNumber(1); +// setHasMore(true); +// }, []); + +// return { +// images, +// allImagesData, +// pageNumber, +// setPageNumber, +// hasMore, +// loading, +// loadingMore, +// fetchImages, +// resetGallery, +// }; +// }; + +// export default useImageGallery; +import { useInfiniteQuery } from "@tanstack/react-query"; + + const PAGE_SIZE = 10; -const useImageGallery = (selectedProjectId) => { - const [images, setImages] = useState([]); - const [allImagesData, setAllImagesData] = useState([]); - const [pageNumber, setPageNumber] = useState(1); - const [hasMore, setHasMore] = useState(true); - const [loading, setLoading] = useState(false); - const [loadingMore, setLoadingMore] = useState(false); - - const fetchImages = useCallback(async (page = 1, filters = {}, reset = false) => { - if (!selectedProjectId) return; - - try { - if (page === 1) { - setLoading(true); - } else { - setLoadingMore(true); - } +const useImageGallery = (selectedProjectId, filters) => { + const hasFilters = filters && Object.values(filters).some( + value => Array.isArray(value) ? value.length > 0 : value !== null && value !== "" + ); + return useInfiniteQuery({ + queryKey: ["imageGallery", selectedProjectId, hasFilters ? filters : null], + enabled: !!selectedProjectId, + getNextPageParam: (lastPage, allPages) => { + if (!lastPage?.data?.length) return undefined; + return allPages.length + 1; + }, + queryFn: async ({ pageParam = 1 }) => { const res = await ImageGalleryAPI.ImagesGet( selectedProjectId, - filters, - page, + hasFilters ? filters : undefined, + pageParam, PAGE_SIZE ); - - const newBatches = res.data || []; - const receivedCount = newBatches.length; - - setImages((prev) => { - if (page === 1 || reset) return newBatches; - const uniqueNew = newBatches.filter( - (batch) => !prev.some((b) => b.batchId === batch.batchId) - ); - return [...prev, ...uniqueNew]; - }); - - setAllImagesData((prev) => { - if (page === 1 || reset) return newBatches; - const uniqueAll = newBatches.filter( - (batch) => !prev.some((b) => b.batchId === batch.batchId) - ); - return [...prev, ...uniqueAll]; - }); - - setHasMore(receivedCount === PAGE_SIZE); - } catch (error) { - console.error("Error fetching images:", error); - if (page === 1) { - setImages([]); - setAllImagesData([]); - } - setHasMore(false); - } finally { - setLoading(false); - setLoadingMore(false); - } - }, [selectedProjectId]); - - const resetGallery = useCallback(() => { - setImages([]); - setAllImagesData([]); - setPageNumber(1); - setHasMore(true); - }, []); - - return { - images, - allImagesData, - pageNumber, - setPageNumber, - hasMore, - loading, - loadingMore, - fetchImages, - resetGallery, - }; + return res; + }, + }); }; -export default useImageGallery; \ No newline at end of file +export default useImageGallery; + diff --git a/src/pages/Gallary/ImageGallary.jsx b/src/pages/Gallary/ImageGallary.jsx index 6d4bd961..16335df2 100644 --- a/src/pages/Gallary/ImageGallary.jsx +++ b/src/pages/Gallary/ImageGallary.jsx @@ -16,32 +16,20 @@ import { setProjectId } from "../../slices/localVariablesSlice"; const SCROLL_THRESHOLD = 5; const ImageGallery = () => { - const selectedProjectId = useSelector((store) => store.localVariables.projectId); + const selectedProjectId = useSelector( + (store) => store.localVariables.projectId + ); + const dispatch = useDispatch(); + const { projectNames } = useProjectName(); - const dispatch = useDispatch() - const { projectNames, loading: projectLoading, fetchData } = useProjectName(); + // Auto-select a project on mount useEffect(() => { - if(selectedProjectId == null){ - dispatch(setProjectId(projectNames[0]?.id)); - } - },[]) - - const { - images, - allImagesData, - pageNumber, - setPageNumber, - hasMore, - loading, - loadingMore, - fetchImages, - resetGallery, - } = useImageGallery(selectedProjectId); - - const { openModal } = useModal(); - - const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD'); + if (!selectedProjectId && projectNames?.length) { + dispatch(setProjectId(projectNames[0].id)); + } + }, [selectedProjectId, projectNames, dispatch]); + // Filter states const [selectedFilters, setSelectedFilters] = useState({ building: [], floor: [], @@ -52,7 +40,6 @@ const ImageGallery = () => { startDate: "", endDate: "", }); - const [appliedFilters, setAppliedFilters] = useState({ buildingIds: null, floorIds: null, @@ -63,7 +50,6 @@ const ImageGallery = () => { startDate: null, endDate: null, }); - const [collapsedFilters, setCollapsedFilters] = useState({ dateRange: false, building: false, @@ -73,7 +59,6 @@ const ImageGallery = () => { workCategory: false, workArea: false, }); - const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false); const [hoveredImage, setHoveredImage] = useState(null); @@ -81,133 +66,113 @@ const ImageGallery = () => { const loaderRef = useRef(null); const filterPanelRef = useRef(null); const filterButtonRef = useRef(null); + const { openModal } = useModal(); + + const { + data, + fetchNextPage, + hasNextPage, + isLoading, + isFetchingNextPage, + refetch, + } = useImageGallery(selectedProjectId, appliedFilters); + + const images = data?.pages.flatMap((page) => page.data) || []; useEffect(() => { - const handleClickOutside = (event) => { + const handleClick = (e) => { if ( filterPanelRef.current && - !filterPanelRef.current.contains(event.target) && + !filterPanelRef.current.contains(e.target) && filterButtonRef.current && - !filterButtonRef.current.contains(event.target) + !filterButtonRef.current.contains(e.target) ) { setIsFilterPanelOpen(false); } }; + document.addEventListener("mousedown", handleClick); + return () => document.removeEventListener("mousedown", handleClick); + }, []); - if (isFilterPanelOpen) { - document.addEventListener("mousedown", handleClickOutside); - } else { - document.removeEventListener("mousedown", handleClickOutside); - } + useEffect(() => { + if (selectedProjectId) refetch(); + }, [selectedProjectId, appliedFilters, refetch]); - return () => { - document.removeEventListener("mousedown", handleClickOutside); + useEffect(() => { + const handler = (data) => { + if (data.projectId === selectedProjectId) refetch(); }; - }, [isFilterPanelOpen]); + eventBus.on("image_gallery", handler); + return () => eventBus.off("image_gallery", handler); + }, [selectedProjectId, refetch]); - useEffect(() => { - if (!selectedProjectId) { - resetGallery(); - return; - } +useEffect(() => { + if (!loaderRef.current) return; - resetGallery(); - fetchImages(1, appliedFilters, true); - }, [selectedProjectId, appliedFilters]); - - useEffect(() => { - const handleExternalEvent = (data) => { - if (selectedProjectId === data.projectId) { - resetGallery(); - fetchImages(1, appliedFilters, true); + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting && hasNextPage && !isFetchingNextPage && !isLoading) { + fetchNextPage(); } - }; + }, + { rootMargin: "200px", threshold: 0.1 } + ); - eventBus.on("image_gallery", handleExternalEvent); + observer.observe(loaderRef.current); - return () => { - eventBus.off("image_gallery", handleExternalEvent); - }; - }, [appliedFilters, fetchImages, selectedProjectId]); + return () => { + if (loaderRef.current) { + observer.unobserve(loaderRef.current); + } + }; +}, [hasNextPage, isFetchingNextPage, isLoading, fetchNextPage]); - useEffect(() => { - if (!loaderRef.current) return; - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasMore && !loadingMore && !loading) { - setPageNumber((prevPageNumber) => prevPageNumber + 1); + // Utility: derive filter options + const getUniqueValues = useCallback( + (idKey, nameKey) => { + const m = new Map(); + images.forEach((batch) => { + const id = idKey === "floorIds" ? batch.floorIds : batch[idKey]; + if (id && batch[nameKey] && !m.has(id)) { + m.set(id, batch[nameKey]); } - }, - { - root: null, - rootMargin: "200px", - threshold: 0.1, - } - ); + }); + return [...m.entries()].sort((a, b) => a[1].localeCompare(b[1])); + }, + [images] + ); - observer.observe(loaderRef.current); - - return () => { - if (loaderRef.current) { - observer.unobserve(loaderRef.current); - } - }; - }, [hasMore, loadingMore, loading]); - - useEffect(() => { - if (pageNumber > 1) { - fetchImages(pageNumber, appliedFilters); - } - }, [pageNumber]); - - const getUniqueValuesWithIds = useCallback((idKey, nameKey) => { - const map = new Map(); - allImagesData.forEach(batch => { - let id = idKey === "floorIds" ? batch.floorIds : batch[idKey]; - const name = batch[nameKey]; - if (id && name && !map.has(id)) { - map.set(id, name); - } - }); - return Array.from(map.entries()).sort((a, b) => a[1].localeCompare(b[1])); - }, [allImagesData]); - - const getUniqueUploadedByUsers = useCallback(() => { - const uniqueUsersMap = new Map(); - allImagesData.forEach(batch => { - batch.documents.forEach(doc => { - if (doc.uploadedBy && doc.uploadedBy.id) { - const fullName = `${doc.uploadedBy.firstName || ""} ${doc.uploadedBy.lastName || ""}`.trim(); - if (fullName) { - uniqueUsersMap.set(doc.uploadedBy.id, fullName); - } + const getUploadedBy = useCallback(() => { + const m = new Map(); + images.forEach((batch) => { + batch.documents.forEach((doc) => { + const name = `${doc.uploadedBy?.firstName || ""} ${ + doc.uploadedBy?.lastName || "" + }`.trim(); + if (doc.uploadedBy?.id && name && !m.has(doc.uploadedBy.id)) { + m.set(doc.uploadedBy.id, name); } }); }); - return Array.from(uniqueUsersMap.entries()).sort((a, b) => a[1].localeCompare(b[1])); - }, [allImagesData]); + return [...m.entries()].sort((a, b) => a[1].localeCompare(b[1])); + }, [images]); - const buildings = getUniqueValuesWithIds("buildingId", "buildingName"); - const floors = getUniqueValuesWithIds("floorIds", "floorName"); - const activities = getUniqueValuesWithIds("activityId", "activityName"); - const workAreas = getUniqueValuesWithIds("workAreaId", "workAreaName"); - const uploadedByUsers = getUniqueUploadedByUsers(); - const workCategories = getUniqueValuesWithIds("workCategoryId", "workCategoryName"); + const buildings = getUniqueValues("buildingId", "buildingName"); + const floors = getUniqueValues("floorIds", "floorName"); + const activities = getUniqueValues("activityId", "activityName"); + const workAreas = getUniqueValues("workAreaId", "workAreaName"); + const workCategories = getUniqueValues("workCategoryId", "workCategoryName"); + const uploadedByUsers = getUploadedBy(); - const toggleFilter = useCallback((type, itemId, itemName) => { + const toggleFilter = useCallback((type, id, name) => { setSelectedFilters((prev) => { - const current = prev[type]; - const isSelected = current.some((item) => item[0] === itemId); - - const newArray = isSelected - ? current.filter((item) => item[0] !== itemId) - : [...current, [itemId, itemName]]; - - return { - ...prev, - [type]: newArray, - }; + const arr = prev[type]; + const exists = arr.some(([x]) => x === id); + const updated = exists + ? arr.filter(([x]) => x !== id) + : [...arr, [id, name]]; + return { ...prev, [type]: updated }; }); }, []); @@ -220,48 +185,33 @@ const ImageGallery = () => { }, []); const toggleCollapse = useCallback((type) => { - setCollapsedFilters((prev) => ({ - ...prev, - [type]: !prev[type], - })); + setCollapsedFilters((prev) => ({ ...prev, [type]: !prev[type] })); }, []); const handleApplyFilters = useCallback(() => { const payload = { - buildingIds: selectedFilters.building.length ? selectedFilters.building.map((item) => item[0]) : null, - floorIds: selectedFilters.floor.length ? selectedFilters.floor.map((item) => item[0]) : null, - workAreaIds: selectedFilters.workArea.length ? selectedFilters.workArea.map((item) => item[0]) : null, - workCategoryIds: selectedFilters.workCategory.length ? selectedFilters.workCategory.map((item) => item[0]) : null, - activityIds: selectedFilters.activity.length ? selectedFilters.activity.map((item) => item[0]) : null, - uploadedByIds: selectedFilters.uploadedBy.length ? selectedFilters.uploadedBy.map((item) => item[0]) : null, + buildingIds: selectedFilters.building.map(([x]) => x) || null, + floorIds: selectedFilters.floor.map(([x]) => x) || null, + activityIds: selectedFilters.activity.map(([x]) => x) || null, + uploadedByIds: selectedFilters.uploadedBy.map(([x]) => x) || null, + workCategoryIds: selectedFilters.workCategory.map(([x]) => x) || null, + workAreaIds: selectedFilters.workArea.map(([x]) => x) || null, startDate: selectedFilters.startDate || null, endDate: selectedFilters.endDate || null, }; - - const areFiltersChanged = Object.keys(payload).some(key => { - const oldVal = appliedFilters[key]; - const newVal = payload[key]; - if (Array.isArray(oldVal) && Array.isArray(newVal)) { - if (oldVal.length !== newVal.length) return true; - const oldSet = new Set(oldVal); - const newSet = new Set(newVal); - for (const item of newSet) { - if (!oldSet.has(item)) return true; - } - return false; - } - if ((oldVal === null && newVal === "") || (oldVal === "" && newVal === null)) return false; - return oldVal !== newVal; + const changed = Object.keys(payload).some((key) => { + const oldVal = appliedFilters[key], + newVal = payload[key]; + return Array.isArray(oldVal) + ? oldVal.length !== newVal.length || + oldVal.some((x) => !newVal.includes(x)) + : oldVal !== newVal; }); - - if (areFiltersChanged) { - setAppliedFilters(payload); - resetGallery(); - } + if (changed) setAppliedFilters(payload); }, [selectedFilters, appliedFilters]); - const handleClearAllFilters = useCallback(() => { - const initialStateSelected = { + const handleClear = useCallback(() => { + setSelectedFilters({ building: [], floor: [], activity: [], @@ -270,10 +220,8 @@ const ImageGallery = () => { workArea: [], startDate: "", endDate: "", - }; - setSelectedFilters(initialStateSelected); - - const initialStateApplied = { + }); + setAppliedFilters({ buildingIds: null, floorIds: null, activityIds: null, @@ -282,69 +230,70 @@ const ImageGallery = () => { workAreaIds: null, startDate: null, endDate: null, - }; - setAppliedFilters(initialStateApplied); - resetGallery(); + }); }, []); - const scrollLeft = useCallback((key) => { - imageGroupRefs.current[key]?.scrollBy({ left: -200, behavior: "smooth" }); - }, []); + const scrollLeft = useCallback( + (key) => + imageGroupRefs.current[key]?.scrollBy({ left: -200, behavior: "smooth" }), + [] + ); + const scrollRight = useCallback( + (key) => + imageGroupRefs.current[key]?.scrollBy({ left: 200, behavior: "smooth" }), + [] + ); - const scrollRight = useCallback((key) => { - imageGroupRefs.current[key]?.scrollBy({ left: 200, behavior: "smooth" }); - }, []); - - const renderFilterCategory = (label, items, type) => ( -