"use client"; import "../../client"; import { cn } from "@/index"; import { ComponentType, ReactNode, useEffect, useRef } from "react"; import { DataListRetriever } from "../../hooks"; import { ModuleWithPermissions } from "../../permissions"; import { ContentTableSearch } from "../tables/ContentTableSearch"; const DEFAULT_GRID_CLASSES = "grid grid-cols-2 gap-4 p-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5"; export type ContentListGridProps = { title?: string; data: DataListRetriever; /** Module identity — used for the header icon and consistency with ContentListTable. */ tableGeneratorType: ModuleWithPermissions; /** Renders one cell per item. The component receives `{ item }`. */ ItemComponent: ComponentType<{ item: T }>; functions?: ReactNode; filters?: ReactNode; /** Default false; when true, renders ContentTableSearch in the header. */ allowSearch?: boolean; /** Drops the rounded outer border, matching ContentListTable's fullWidth behaviour. */ fullWidth?: boolean; /** Replaces the default responsive grid classes entirely when set. */ gridClassName?: string; }; export function ContentListGrid(props: ContentListGridProps) { const { data, ItemComponent, allowSearch, fullWidth, gridClassName } = props; const sentinelRef = useRef(null); useEffect(() => { if (!data.next || !sentinelRef.current) return; const observer = new IntersectionObserver( (entries) => { if (entries[0]?.isIntersecting) data.next?.(); }, { threshold: 0.1, rootMargin: "200px" }, ); observer.observe(sentinelRef.current); return () => observer.disconnect(); }, [data.next]); return (
{props.title !== undefined && (
{props.tableGeneratorType.icon && ( )} {props.title}
{(props.functions || props.filters || allowSearch) && ( <> {props.functions} {props.filters} {allowSearch && } )}
)} {data.data && data.data.length > 0 ? (
{data.data.map((item) => { const id = (item as unknown as { id: string }).id; return ; })}
) : (
No results.
)} {data.next &&
}
); }