@adventurelabs/scout-core
Version:
Core utilities and helpers for Adventure Labs Scout applications
433 lines (432 loc) • 16.3 kB
JavaScript
import { useState, useCallback, useMemo, useEffect, useRef } from "react";
import { useGetSessionsInfiniteByHerdQuery, useGetSessionsInfiniteByDeviceQuery, useGetEventsInfiniteByHerdQuery, useGetEventsInfiniteByDeviceQuery, useGetArtifactsInfiniteByHerdQuery, useGetArtifactsInfiniteByDeviceQuery, } from "../store/api";
// =====================================================
// SESSIONS INFINITE SCROLL HOOKS
// =====================================================
export const useInfiniteSessionsByHerd = (herdId, options) => {
const [pages, setPages] = useState([]);
const [currentCursor, setCurrentCursor] = useState(null);
const prevHerdIdRef = useRef();
const currentQuery = useGetSessionsInfiniteByHerdQuery({
herdId,
limit: options.limit || 20,
cursor: currentCursor,
supabase: options.supabase,
}, {
skip: !options.enabled || !herdId,
});
// Reset state when herdId changes
useEffect(() => {
if (prevHerdIdRef.current !== undefined &&
prevHerdIdRef.current !== herdId &&
options.enabled &&
herdId) {
setPages([]);
setCurrentCursor(null);
}
prevHerdIdRef.current = herdId;
}, [herdId, options.enabled]);
// Update pages when new data arrives
useEffect(() => {
if (currentQuery.data && !currentQuery.isLoading) {
setPages((prev) => {
const existingPage = prev.find((p) => (p.cursor === null && currentCursor === null) ||
(p.cursor &&
currentCursor &&
p.cursor.id === currentCursor.id &&
p.cursor.timestamp === currentCursor.timestamp));
if (!existingPage) {
return [
...prev,
{ cursor: currentCursor, data: currentQuery.data.sessions },
];
}
return prev;
});
}
}, [currentQuery.data, currentQuery.isLoading, currentCursor]);
const loadMore = useCallback(() => {
if (currentQuery.data?.hasMore &&
currentQuery.data.nextCursor &&
!currentQuery.isLoading) {
setCurrentCursor(currentQuery.data.nextCursor);
}
}, [currentQuery.data, currentQuery.isLoading]);
const refetch = useCallback(() => {
setPages([]);
setCurrentCursor(null);
currentQuery.refetch();
}, [currentQuery]);
// Flatten all pages into single array
const allItems = useMemo(() => {
return pages.flatMap((page) => page.data);
}, [pages]);
return {
items: allItems,
isLoading: currentQuery.isLoading && pages.length === 0,
isLoadingMore: currentQuery.isLoading && pages.length > 0,
hasMore: currentQuery.data?.hasMore || false,
loadMore,
refetch,
error: currentQuery.error,
};
};
export const useInfiniteSessionsByDevice = (deviceId, options) => {
const [pages, setPages] = useState([]);
const [currentCursor, setCurrentCursor] = useState(null);
const prevDeviceIdRef = useRef();
const currentQuery = useGetSessionsInfiniteByDeviceQuery({
deviceId,
limit: options.limit || 20,
cursor: currentCursor,
supabase: options.supabase,
}, {
skip: !options.enabled || !deviceId,
});
// Reset state when deviceId changes
useEffect(() => {
if (prevDeviceIdRef.current !== undefined &&
prevDeviceIdRef.current !== deviceId &&
options.enabled &&
deviceId) {
setPages([]);
setCurrentCursor(null);
}
prevDeviceIdRef.current = deviceId;
}, [deviceId, options.enabled]);
useEffect(() => {
if (currentQuery.data && !currentQuery.isLoading) {
setPages((prev) => {
const existingPage = prev.find((p) => (p.cursor === null && currentCursor === null) ||
(p.cursor &&
currentCursor &&
p.cursor.id === currentCursor.id &&
p.cursor.timestamp === currentCursor.timestamp));
if (!existingPage) {
return [
...prev,
{ cursor: currentCursor, data: currentQuery.data.sessions },
];
}
return prev;
});
}
}, [currentQuery.data, currentQuery.isLoading, currentCursor]);
const loadMore = useCallback(() => {
if (currentQuery.data?.hasMore &&
currentQuery.data.nextCursor &&
!currentQuery.isLoading) {
setCurrentCursor(currentQuery.data.nextCursor);
}
}, [currentQuery.data, currentQuery.isLoading]);
const refetch = useCallback(() => {
setPages([]);
setCurrentCursor(null);
currentQuery.refetch();
}, [currentQuery]);
const allItems = useMemo(() => {
return pages.flatMap((page) => page.data);
}, [pages]);
return {
items: allItems,
isLoading: currentQuery.isLoading && pages.length === 0,
isLoadingMore: currentQuery.isLoading && pages.length > 0,
hasMore: currentQuery.data?.hasMore || false,
loadMore,
refetch,
error: currentQuery.error,
};
};
// =====================================================
// EVENTS INFINITE SCROLL HOOKS
// =====================================================
export const useInfiniteEventsByHerd = (herdId, options) => {
const [pages, setPages] = useState([]);
const [currentCursor, setCurrentCursor] = useState(null);
const prevHerdIdRef = useRef();
const currentQuery = useGetEventsInfiniteByHerdQuery({
herdId,
limit: options.limit || 20,
cursor: currentCursor,
supabase: options.supabase,
}, {
skip: !options.enabled || !herdId,
});
// Reset state when herdId changes
useEffect(() => {
if (prevHerdIdRef.current !== undefined &&
prevHerdIdRef.current !== herdId &&
options.enabled &&
herdId) {
setPages([]);
setCurrentCursor(null);
}
prevHerdIdRef.current = herdId;
}, [herdId, options.enabled]);
useEffect(() => {
if (currentQuery.data && !currentQuery.isLoading) {
setPages((prev) => {
const existingPage = prev.find((p) => (p.cursor === null && currentCursor === null) ||
(p.cursor &&
currentCursor &&
p.cursor.id === currentCursor.id &&
p.cursor.timestamp === currentCursor.timestamp));
if (!existingPage) {
return [
...prev,
{ cursor: currentCursor, data: currentQuery.data.events },
];
}
return prev;
});
}
}, [currentQuery.data, currentQuery.isLoading, currentCursor]);
const loadMore = useCallback(() => {
if (currentQuery.data?.hasMore &&
currentQuery.data.nextCursor &&
!currentQuery.isLoading) {
setCurrentCursor(currentQuery.data.nextCursor);
}
}, [currentQuery.data, currentQuery.isLoading]);
const refetch = useCallback(() => {
setPages([]);
setCurrentCursor(null);
currentQuery.refetch();
}, [currentQuery]);
const allItems = useMemo(() => {
return pages.flatMap((page) => page.data);
}, [pages]);
return {
items: allItems,
isLoading: currentQuery.isLoading && pages.length === 0,
isLoadingMore: currentQuery.isLoading && pages.length > 0,
hasMore: currentQuery.data?.hasMore || false,
loadMore,
refetch,
error: currentQuery.error,
};
};
export const useInfiniteEventsByDevice = (deviceId, options) => {
const [pages, setPages] = useState([]);
const [currentCursor, setCurrentCursor] = useState(null);
const prevDeviceIdRef = useRef();
const currentQuery = useGetEventsInfiniteByDeviceQuery({
deviceId,
limit: options.limit || 20,
cursor: currentCursor,
supabase: options.supabase,
}, {
skip: !options.enabled || !deviceId,
});
// Reset state when deviceId changes
useEffect(() => {
if (prevDeviceIdRef.current !== undefined &&
prevDeviceIdRef.current !== deviceId &&
options.enabled &&
deviceId) {
setPages([]);
setCurrentCursor(null);
}
prevDeviceIdRef.current = deviceId;
}, [deviceId, options.enabled]);
useEffect(() => {
if (currentQuery.data && !currentQuery.isLoading) {
setPages((prev) => {
const existingPage = prev.find((p) => (p.cursor === null && currentCursor === null) ||
(p.cursor &&
currentCursor &&
p.cursor.id === currentCursor.id &&
p.cursor.timestamp === currentCursor.timestamp));
if (!existingPage) {
return [
...prev,
{ cursor: currentCursor, data: currentQuery.data.events },
];
}
return prev;
});
}
}, [currentQuery.data, currentQuery.isLoading, currentCursor]);
const loadMore = useCallback(() => {
if (currentQuery.data?.hasMore &&
currentQuery.data.nextCursor &&
!currentQuery.isLoading) {
setCurrentCursor(currentQuery.data.nextCursor);
}
}, [currentQuery.data, currentQuery.isLoading]);
const refetch = useCallback(() => {
setPages([]);
setCurrentCursor(null);
currentQuery.refetch();
}, [currentQuery]);
const allItems = useMemo(() => {
return pages.flatMap((page) => page.data);
}, [pages]);
return {
items: allItems,
isLoading: currentQuery.isLoading && pages.length === 0,
isLoadingMore: currentQuery.isLoading && pages.length > 0,
hasMore: currentQuery.data?.hasMore || false,
loadMore,
refetch,
error: currentQuery.error,
};
};
// =====================================================
// ARTIFACTS INFINITE SCROLL HOOKS
// =====================================================
export const useInfiniteArtifactsByHerd = (herdId, options) => {
const [pages, setPages] = useState([]);
const [currentCursor, setCurrentCursor] = useState(null);
const prevHerdIdRef = useRef();
const currentQuery = useGetArtifactsInfiniteByHerdQuery({
herdId,
limit: options.limit || 20,
cursor: currentCursor,
supabase: options.supabase,
}, {
skip: !options.enabled || !herdId,
});
// Reset state when herdId changes
useEffect(() => {
if (prevHerdIdRef.current !== undefined &&
prevHerdIdRef.current !== herdId &&
options.enabled &&
herdId) {
setPages([]);
setCurrentCursor(null);
}
prevHerdIdRef.current = herdId;
}, [herdId, options.enabled]);
useEffect(() => {
if (currentQuery.data && !currentQuery.isLoading) {
setPages((prev) => {
const existingPage = prev.find((p) => (p.cursor === null && currentCursor === null) ||
(p.cursor &&
currentCursor &&
p.cursor.id === currentCursor.id &&
p.cursor.timestamp === currentCursor.timestamp));
if (!existingPage) {
return [
...prev,
{ cursor: currentCursor, data: currentQuery.data.artifacts },
];
}
return prev;
});
}
}, [currentQuery.data, currentQuery.isLoading, currentCursor]);
const loadMore = useCallback(() => {
if (currentQuery.data?.hasMore &&
currentQuery.data.nextCursor &&
!currentQuery.isLoading) {
setCurrentCursor(currentQuery.data.nextCursor);
}
}, [currentQuery.data, currentQuery.isLoading]);
const refetch = useCallback(() => {
setPages([]);
setCurrentCursor(null);
currentQuery.refetch();
}, [currentQuery]);
const allItems = useMemo(() => {
return pages.flatMap((page) => page.data);
}, [pages]);
return {
items: allItems,
isLoading: currentQuery.isLoading && pages.length === 0,
isLoadingMore: currentQuery.isLoading && pages.length > 0,
hasMore: currentQuery.data?.hasMore || false,
loadMore,
refetch,
error: currentQuery.error,
};
};
export const useInfiniteArtifactsByDevice = (deviceId, options) => {
const [pages, setPages] = useState([]);
const [currentCursor, setCurrentCursor] = useState(null);
const prevDeviceIdRef = useRef();
const currentQuery = useGetArtifactsInfiniteByDeviceQuery({
deviceId,
limit: options.limit || 20,
cursor: currentCursor,
supabase: options.supabase,
}, {
skip: !options.enabled || !deviceId,
});
// Reset state when deviceId changes
useEffect(() => {
if (prevDeviceIdRef.current !== undefined &&
prevDeviceIdRef.current !== deviceId &&
options.enabled &&
deviceId) {
setPages([]);
setCurrentCursor(null);
}
prevDeviceIdRef.current = deviceId;
}, [deviceId, options.enabled]);
useEffect(() => {
if (currentQuery.data && !currentQuery.isLoading) {
setPages((prev) => {
const existingPage = prev.find((p) => (p.cursor === null && currentCursor === null) ||
(p.cursor &&
currentCursor &&
p.cursor.id === currentCursor.id &&
p.cursor.timestamp === currentCursor.timestamp));
if (!existingPage) {
return [
...prev,
{ cursor: currentCursor, data: currentQuery.data.artifacts },
];
}
return prev;
});
}
}, [currentQuery.data, currentQuery.isLoading, currentCursor]);
const loadMore = useCallback(() => {
if (currentQuery.data?.hasMore &&
currentQuery.data.nextCursor &&
!currentQuery.isLoading) {
setCurrentCursor(currentQuery.data.nextCursor);
}
}, [currentQuery.data, currentQuery.isLoading]);
const refetch = useCallback(() => {
setPages([]);
setCurrentCursor(null);
currentQuery.refetch();
}, [currentQuery]);
const allItems = useMemo(() => {
return pages.flatMap((page) => page.data);
}, [pages]);
return {
items: allItems,
isLoading: currentQuery.isLoading && pages.length === 0,
isLoadingMore: currentQuery.isLoading && pages.length > 0,
hasMore: currentQuery.data?.hasMore || false,
loadMore,
refetch,
error: currentQuery.error,
};
};
// =====================================================
// INTERSECTION OBSERVER HOOK FOR AUTO-LOADING
// =====================================================
export const useIntersectionObserver = (callback, options = {}) => {
const [element, setElement] = useState(null);
useEffect(() => {
if (!element)
return;
const observer = new IntersectionObserver((entries) => {
const first = entries[0];
if (first.isIntersecting) {
callback();
}
}, { threshold: 0.1, ...options });
observer.observe(element);
return () => {
if (element) {
observer.unobserve(element);
}
};
}, [element, callback, options]);
return setElement;
};