snappier animations

This commit is contained in:
Nate Kelley 2025-10-07 21:43:43 -06:00
parent 79d5fba6e3
commit d6e3ab9a51
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
5 changed files with 57 additions and 56 deletions

View File

@ -18,38 +18,33 @@ export const useSearch = <T = SearchTextResponse>(
});
};
export const useSearchInfinite = (
{
enabled,
mounted,
...params
}: Pick<
Parameters<typeof search>[0],
| 'page_size'
| 'assetTypes'
| 'includeAssetAncestors'
| 'includeScreenshots'
| 'endDate'
| 'startDate'
> & {
scrollConfig?: Parameters<typeof useInfiniteScroll>[0]['scrollConfig'];
searchQuery: string;
enabled?: boolean;
mounted?: boolean;
} = {
page_size: 5,
assetTypes: ['chat'],
searchQuery: '',
enabled: true,
mounted: true,
}
) => {
export const useSearchInfinite = ({
enabled,
mounted,
scrollConfig,
...params
}: Pick<
Parameters<typeof search>[0],
| 'page_size'
| 'assetTypes'
| 'includeAssetAncestors'
| 'includeScreenshots'
| 'endDate'
| 'startDate'
> & {
scrollConfig?: Parameters<typeof useInfiniteScroll>[0]['scrollConfig'];
searchQuery: string;
enabled?: boolean;
mounted?: boolean;
}) => {
const { searchQuery } = params;
return useInfiniteScroll<SearchTextData>({
queryKey: ['search', 'results', 'infinite', params] as const,
staleTime: 1000 * 30, // 30 seconds
staleTime: 1000 * 45, // 45 seconds
queryFn: ({ pageParam = 1 }) => search({ query: searchQuery, page: pageParam, ...params }),
placeholderData: keepPreviousData,
enabled: true,
enabled,
scrollConfig,
mounted,
});
};

View File

@ -6,8 +6,9 @@ import type {
UseInfiniteQueryResult,
} from '@tanstack/react-query';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useEffect, useRef } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import type { ApiError } from '@/api/errors';
import { useDebounce } from '@/hooks/useDebounce';
/**
* Configuration for infinite scroll behavior
@ -41,6 +42,7 @@ type UseInfiniteScrollOptions<TData, TError = ApiError> = Omit<
* Infinite scroll configuration
*/
scrollConfig?: InfiniteScrollConfig;
mounted?: boolean;
};
/**
@ -82,11 +84,12 @@ type UseInfiniteScrollResult<TData, TError = ApiError> = UseInfiniteQueryResult<
export function useInfiniteScroll<TData, TError = ApiError>(
options: UseInfiniteScrollOptions<TData, TError>
): UseInfiniteScrollResult<TData, TError> {
const { scrollConfig, ...queryOptions } = options;
const { scrollConfig, mounted, ...queryOptions } = options;
const scrollThreshold =
scrollConfig?.scrollThreshold ?? InfiniteScrollConfigSchema.scrollThreshold;
const scrollContainerRef = useRef<HTMLDivElement>(null);
const debouncedMounted = useDebounce(mounted, { wait: 2000 });
const queryResult = useInfiniteQuery({
...queryOptions,
@ -102,7 +105,10 @@ export function useInfiniteScroll<TData, TError = ApiError>(
const { fetchNextPage, hasNextPage, isFetchingNextPage } = queryResult;
// Combine all pages into a single array of results
const allResults = queryResult.data?.pages.flatMap((page) => page.data) ?? [];
const allResults = useMemo(
() => queryResult.data?.pages.flatMap((page) => page.data) ?? [],
[queryResult.data]
);
useEffect(() => {
const container = scrollContainerRef.current;
@ -123,14 +129,7 @@ export function useInfiniteScroll<TData, TError = ApiError>(
container.addEventListener('scroll', handleScroll);
return () => container.removeEventListener('scroll', handleScroll);
}, [
hasNextPage,
isFetchingNextPage,
fetchNextPage,
scrollThreshold,
queryResult.isFetched,
options.enabled,
]);
}, [hasNextPage, isFetchingNextPage, fetchNextPage, scrollThreshold, debouncedMounted]);
return {
...queryResult,

View File

@ -54,7 +54,7 @@ export const GlobalSearchModal = () => {
includeAssetAncestors: true,
includeScreenshots: true,
scrollConfig: {
scrollThreshold: 10,
scrollThreshold: 55,
},
mounted: isOpen,
});

View File

@ -59,28 +59,31 @@ export const GlobalSearchSecondaryContent: React.FC<GlobalSearchSecondaryContent
};
function getFallback(assetType: SearchTextData['assetType']) {
switch (assetType) {
case 'chat':
return SkeletonSearchChat;
case 'metric_file':
return SkeletonSearchMetric;
case 'dashboard_file':
return SkeletonSearchDashboard;
case 'report_file':
return SkeletonSearchReport;
case 'collection':
return SkeletonSearchMetric;
default:
return SkeletonSearchMetric;
if (assetType === 'metric_file') {
return SkeletonSearchMetric;
} else if (assetType === 'chat') {
return SkeletonSearchChat;
} else if (assetType === 'dashboard_file') {
return SkeletonSearchDashboard;
} else if (assetType === 'report_file') {
return SkeletonSearchReport;
} else if (assetType === 'collection') {
return SkeletonSearchMetric;
} else {
const _exhaustiveCheck: never = assetType;
console.warn('Exhaustive check', _exhaustiveCheck);
return SkeletonSearchMetric;
}
}
const ScreenshotImage = ({
screenshotUrl,
assetType,
className,
}: {
screenshotUrl: string | null | undefined;
assetType: SearchTextData['assetType'];
className?: string;
}) => {
const [isLoaded, setIsLoaded] = useState(false);
const [isCached, setIsCached] = useState(false);
@ -129,7 +132,7 @@ const ScreenshotImage = ({
key={imageUrl}
src={imageUrl}
alt="Screenshot"
className="w-full h-full object-cover object-top"
className={cn('w-full h-full object-cover object-top', className)}
initial={
isCached ? { opacity: 1, filter: 'blur(0px)' } : { opacity: 0, filter: 'blur(4px)' }
}
@ -172,7 +175,11 @@ const MetricScreenshotContainer = ({
transition={{ duration: 0.2, ease: 'easeOut' }}
>
{isLoadingContent ? (
<ScreenshotImage screenshotUrl={screenshotUrl} assetType={'metric_file'} />
<ScreenshotImage
screenshotUrl={screenshotUrl}
assetType={'metric_file'}
className="animate-pulse"
/>
) : (
<MetricChartCard
metricId={assetId}

View File

@ -4,7 +4,7 @@ import { cn } from '@/lib/utils';
import { SearchModalContentItems } from './SearchModalContentItems';
import type { SearchItem, SearchItems, SearchModalContentProps } from './search-modal.types';
const duration = 0.15;
const duration = 0.12;
export const SearchModalItemsContainer = <M, T extends string>({
searchItems,